diff --git a/adapters/copilot-adapter.ts b/adapters/copilot-adapter.ts new file mode 100644 index 0000000..0e88ff4 --- /dev/null +++ b/adapters/copilot-adapter.ts @@ -0,0 +1,289 @@ +#!/usr/bin/env bun +/** + * CopilotAdapter — MCP adapter for Copilot CLI instances. + * + * Nearly identical to CodexAdapter: no push capability. Messages are + * delivered via file-based inbox + piggybacked on tool responses. + */ + +import { BaseAdapter } from "./base-adapter.ts"; +import type { BufferedMessage } from "../shared/types.ts"; +import * as fs from "node:fs"; +import * as path from "node:path"; + +export class CopilotAdapter extends BaseAdapter { + private messageBuffer: BufferedMessage[] = []; + private inboxPath: string | null = null; + private hasRunStartup = false; + + constructor() { + super("copilot"); + } + + getCapabilities(): Record { + return { + tools: {}, + }; + } + + getSystemPrompt(): string { + const inboxHint = this.inboxPath + ? `\nYour INBOX FILE: ${this.inboxPath}` + : "\nYour INBOX FILE: .multiagents/inbox/.md (created after registration)"; + + const base = `You are GitHub Copilot CLI (backed by GPT-5.4) on the multiagents network. + +⚠ IMPORTANT: Copilot executes long shell commands (build, tests, refactors) BETWEEN tool calls. +You MUST periodically read \`.multiagents/inbox/.md\` — do NOT rely only on MCP piggyback. +After every shell command longer than 10s, re-check the inbox. + +═══ COMMUNICATION IS YOUR #1 PRIORITY ═══ + +You have MCP tools from the "multiagents-peer" server. USE THEM — not shell commands, not HTTP calls, not CLI scripts. The MCP tools are: + set_summary, check_messages, send_message, check_team_status, get_plan, + update_plan, signal_done, submit_feedback, approve, list_peers, + acquire_file, release_file, view_file_locks, get_history + +BEFORE ANY OTHER WORK — call these MCP tools in this exact order: + 1. set_summary → describe your task in one line + 2. check_team_status → see your teammates and their roles + 3. get_plan → see the plan and your assigned items + 4. check_messages → read pending messages from teammates + +AFTER EVERY SHELL COMMAND OR FILE WRITE: + → Call check_messages (teammates may have sent critical updates) + → Call set_summary with what you just did + +AFTER EVERY 2-3 TOOL CALLS: + → Read your inbox file for real-time messages from teammates${inboxHint} + +WHEN YOU FINISH YOUR TASK: + → Call signal_done with what you built, tested, and results + → Then call check_messages every 10s waiting for review feedback + +WHEN A TEAMMATE MESSAGES YOU: + → Reply via send_message IMMEDIATELY — they are blocked waiting on you + +DO NOT: + ✗ Work for 1+ minute without calling check_messages + ✗ Finish without calling signal_done + ✗ Ignore teammate messages + ✗ Try to use CLI/HTTP to talk to the broker — use MCP tools only + ✗ Go silent — if you have nothing to do, call check_team_status and set_summary "Idle, ready to help" + +QUALITY: Production-grade code. Plan before coding. Verify before signaling done.`; + + if (this.roleContext) { + return `${base}\n\n--- ROLE CONTEXT ---\n${this.roleContext}`; + } + return base; + } + + /** + * Deliver a message by both buffering (for piggyback) and writing to + * the file-based inbox so Copilot sees it via native file reads. + */ + async deliverMessage(msg: BufferedMessage): Promise { + this.messageBuffer.push(msg); + await this.writeToInbox(msg); + } + + // --- File-based inbox --- + + private ensureInboxPath(): string | null { + if (this.inboxPath) return this.inboxPath; + + const slotId = this.mySlot?.id; + const name = this.mySlot?.display_name ?? (slotId ? `slot-${slotId}` : null); + if (!name) return null; + + const inboxDir = path.join(process.cwd(), ".multiagents", "inbox"); + try { + if (!fs.existsSync(inboxDir)) { + fs.mkdirSync(inboxDir, { recursive: true }); + } + } catch { + return null; + } + + const safeName = path.basename(name).replace(/[^a-zA-Z0-9_\-. ]/g, "_"); + this.inboxPath = path.join(inboxDir, `${safeName}.md`); + + try { + fs.writeFileSync(this.inboxPath, `# Inbox for ${name}\n\nMessages from teammates appear below. Newest at bottom.\n\n---\n\n`); + } catch { /* best effort */ } + + return this.inboxPath; + } + + private async writeToInbox(msg: BufferedMessage): Promise { + const inboxPath = this.ensureInboxPath(); + if (!inboxPath) return; + + try { + const timestamp = new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" }); + const from = msg.from_display_name ?? msg.from_role ?? msg.from_id; + const entry = `**[${timestamp}] ${from}** (${msg.msg_type}):\n${msg.text}\n\n---\n\n`; + fs.appendFileSync(inboxPath, entry); + } catch { /* best effort */ } + } + + // --- Auto-startup: inject team context on first MCP tool call --- + + private async runAutoStartup(): Promise { + if (this.hasRunStartup) return ""; + this.hasRunStartup = true; + + const parts: string[] = [ + "╔══════════════════════════════════════════════════════════╗", + "║ AUTO-STARTUP: Team context injected on first tool call ║", + "╚══════════════════════════════════════════════════════════╝", + "", + ]; + + try { + const role = this.mySlot?.role ?? "team member"; + const name = this.mySlot?.display_name ?? "agent"; + await this.broker.setSummary(this.myId!, `${name} (${role}) — starting up, reading team context`); + parts.push(`✓ set_summary: Registered as ${name} (${role})`); + } catch (e) { + parts.push(`✗ set_summary failed: ${e instanceof Error ? e.message : String(e)}`); + } + + try { + const teamResult = await this.handleCheckTeamStatus(); + parts.push("", "── TEAM STATUS ──", teamResult.content[0]?.text ?? ""); + } catch { parts.push("✗ check_team_status failed"); } + + try { + const planResult = await this.handleGetPlan(); + parts.push("", "── YOUR PLAN ──", planResult.content[0]?.text ?? ""); + } catch { parts.push("✗ get_plan failed"); } + + try { + const msgResult = await this.handleCheckMessages(); + parts.push("", "── PENDING MESSAGES ──", msgResult.content[0]?.text ?? ""); + } catch { parts.push("✗ check_messages failed"); } + + parts.push( + "", + "── ACTION REQUIRED ──", + "Use multiagents-peer MCP tools to communicate:", + " → set_summary after every major action", + " → check_messages after every shell command", + " → send_message to reply to teammates", + " → signal_done when finished (with proof)", + " → update_plan to mark items in_progress/done", + "DO NOT go 1+ minute without calling check_messages.", + "════════════════════════════════════════════════════════════", + "", + ); + + return parts.join("\n"); + } + + protected wrapToolResult(result: string): string { + if (this.messageBuffer.length === 0) { + return result; + } + + const pending = this.messageBuffer + .map((m) => this.formatMessage(m)) + .join("\n"); + this.messageBuffer = []; + + return `[PENDING MESSAGES]\n${pending}\n[END PENDING MESSAGES]\n\n${result}`; + } + + protected override async handleListPeers(args: any) { + const r = await super.handleListPeers(args); + return this.wrapResult(r); + } + + protected override async handleSendMessage(args: any) { + const r = await super.handleSendMessage(args); + return this.wrapResult(r); + } + + protected override async handleSetSummary(args: any) { + const r = await super.handleSetSummary(args); + return this.wrapResult(r); + } + + protected override async handleCheckMessages() { + const r = await super.handleCheckMessages(); + return this.wrapResult(r); + } + + protected override async handleAssignRole(args: any) { + const r = await super.handleAssignRole(args); + return this.wrapResult(r); + } + + protected override async handleRenamePeer(args: any) { + const r = await super.handleRenamePeer(args); + return this.wrapResult(r); + } + + protected override async handleAcquireFile(args: any) { + const r = await super.handleAcquireFile(args); + return this.wrapResult(r); + } + + protected override async handleReleaseFile(args: any) { + const r = await super.handleReleaseFile(args); + return this.wrapResult(r); + } + + protected override async handleViewFileLocks() { + const r = await super.handleViewFileLocks(); + return this.wrapResult(r); + } + + protected override async handleGetHistory(args: any) { + const r = await super.handleGetHistory(args); + return this.wrapResult(r); + } + + // --- Lifecycle tool wrappers (critical for message delivery) --- + + protected override async handleSignalDone(args: any) { + const r = await super.handleSignalDone(args); + return this.wrapResult(r); + } + + protected override async handleSubmitFeedback(args: any) { + const r = await super.handleSubmitFeedback(args); + return this.wrapResult(r); + } + + protected override async handleApprove(args: any) { + const r = await super.handleApprove(args); + return this.wrapResult(r); + } + + protected override async handleCheckTeamStatus() { + const r = await super.handleCheckTeamStatus(); + return this.wrapResult(r); + } + + protected override async handleGetPlan() { + const r = await super.handleGetPlan(); + return this.wrapResult(r); + } + + protected override async handleUpdatePlan(args: any) { + const r = await super.handleUpdatePlan(args); + return this.wrapResult(r); + } + + private async wrapResult(result: { content: { type: string; text: string }[]; isError?: boolean }) { + const text = result.content[0]?.text ?? ""; + const startupContext = await this.runAutoStartup(); + const wrapped = this.wrapToolResult(text); + return { + ...result, + content: [{ type: "text" as const, text: startupContext + wrapped }], + }; + } +} diff --git a/adapters/jinn-adapter.ts b/adapters/jinn-adapter.ts new file mode 100644 index 0000000..345c832 --- /dev/null +++ b/adapters/jinn-adapter.ts @@ -0,0 +1,137 @@ +#!/usr/bin/env bun +/** + * JinnAdapter — MCP adapter acting as a bridge to the Jinn Gateway. + * + * Exposes the 13 Jinn AI employees as virtual peers on the multiagents + * network. Inbound messages are routed via HTTP POST to the local Jinn + * Gateway (default port 7777). This adapter does not run in driver mode; + * it relies on the broker's polling loop like a standard BaseAdapter. + */ + +import { BaseAdapter } from "./base-adapter.ts"; +import type { BufferedMessage } from "../shared/types.ts"; + +const JINN_GATEWAY_URL = process.env.JINN_GATEWAY_URL ?? "http://127.0.0.1:7777"; + +const EMPLOYEES: readonly string[] = [ + "gpt-coder", + "kimi-coder", + "deep-reasoner", + "deepseek-analyst", + "code-reviewer", + "remi-cohere", + "sam-cerebras", + "secops", + "sysadmin", + "tech-watch", + "diane-nim", + "kilocode-coder", + "alibaba-coder", +]; + +// TODO: confirmer l'endpoint exact de l'API Jinn Gateway (delegate vs sessions) + +export class JinnAdapter extends BaseAdapter { + private messageBuffer: BufferedMessage[] = []; + + constructor() { + super("jinn"); + } + + getCapabilities(): Record { + return { + tools: {}, + }; + } + + getSystemPrompt(): string { + const base = `You are a bridge adapter to the Jinn Gateway multi-AI team. +Your role is to proxy messages between the multiagents network and the +13 Jinn employees running on the local Gateway (${JINN_GATEWAY_URL}). + +VIRTUAL EMPLOYEES — mention one with @ in your message text: + • gpt-coder — GPT-5.4 (Copilot) + • kimi-coder — Kimi K2.6 (Moonshot) + • deep-reasoner — DeepSeek R1 (reasoning) + • deepseek-analyst — DeepSeek V3.2 (analysis) + • code-reviewer — Mistral Large (review) + • remi-cohere — Cohere Command A + • sam-cerebras — Cerebras Qwen3-235B + • secops — Hermes-4-70B (security) + • sysadmin — Qwen3-235B (OpenRouter) + • tech-watch — Gemini 2.5 Pro (R&D watch) + • diane-nim — Nemotron Ultra 253B (NIM) + • kilocode-coder — Claude Opus 4 (KiloCode) + • alibaba-coder — Qwen3-coder-next (DashScope) + +ROUTING RULES: + → Prefix your message with @ to choose a recipient. + → If you omit the mention, the gateway defaults to gpt-coder. + → Do NOT call Jinn HTTP endpoints yourself — use send_message. + +MCP TOOLS: set_summary, check_messages, send_message, check_team_status, +get_plan, update_plan, signal_done, submit_feedback, approve, list_peers, +acquire_file, release_file, view_file_locks, get_history`; + + if (this.roleContext) { + return `${base}\n\n--- ROLE CONTEXT ---\n${this.roleContext}`; + } + return base; + } + + async deliverMessage(msg: BufferedMessage): Promise { + // Retry previously failed deliveries first + const backlog = [...this.messageBuffer]; + this.messageBuffer = []; + for (const m of backlog) { + await this.dispatch(m); + } + await this.dispatch(msg); + } + + private async dispatch(msg: BufferedMessage): Promise { + const employee = this.resolveEmployee(msg); + const payload = { + employee, + task: msg.text, + from_id: msg.from_id, + from_role: msg.from_role, + msg_type: msg.msg_type, + }; + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 10000); + + try { + const res = await fetch(`${JINN_GATEWAY_URL}/delegate`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + signal: controller.signal, + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`HTTP ${res.status}: ${text}`); + } + } catch (err) { + const error = err instanceof Error ? err.message : String(err); + console.error(`[JinnAdapter] Failed to route to ${employee}: ${error}`); + this.messageBuffer.push(msg); + } finally { + clearTimeout(timeout); + } + } + + private resolveEmployee(msg: BufferedMessage): string { + const mention = msg.text.match(/@([a-zA-Z0-9_-]+)/); + const mentioned = mention?.[1]; + if (mentioned && (EMPLOYEES as readonly string[]).includes(mentioned)) { + return mentioned; + } + if (msg.to_id && (EMPLOYEES as readonly string[]).includes(msg.to_id)) { + return msg.to_id; + } + return "gpt-coder"; + } +} diff --git a/adapters/kimi-adapter.ts b/adapters/kimi-adapter.ts new file mode 100644 index 0000000..378961f --- /dev/null +++ b/adapters/kimi-adapter.ts @@ -0,0 +1,286 @@ +#!/usr/bin/env bun +/** + * KimiAdapter — MCP adapter for Kimi CLI instances. + * + * Nearly identical to CodexAdapter: no push capability. Messages are + * delivered via file-based inbox + piggybacked on tool responses. + */ + +import { BaseAdapter } from "./base-adapter.ts"; +import type { BufferedMessage } from "../shared/types.ts"; +import * as fs from "node:fs"; +import * as path from "node:path"; + +export class KimiAdapter extends BaseAdapter { + private messageBuffer: BufferedMessage[] = []; + private inboxPath: string | null = null; + private hasRunStartup = false; + + constructor() { + super("kimi"); + } + + getCapabilities(): Record { + return { + tools: {}, + }; + } + + getSystemPrompt(): string { + const inboxHint = this.inboxPath + ? `\nYour INBOX FILE: ${this.inboxPath}` + : "\nYour INBOX FILE: .multiagents/inbox/.md (created after registration)"; + + const base = `You are a Kimi K2.6 instance (Moonshot AI, reasoning always-on, 262K context window) on the multiagents network. +You are a team member on the multiagents network. You MUST use the multiagents-peer MCP tools to communicate. Your team CANNOT see your work unless you tell them. + +═══ COMMUNICATION IS YOUR #1 PRIORITY ═══ + +You have MCP tools from the "multiagents-peer" server. USE THEM — not shell commands, not HTTP calls, not CLI scripts. The MCP tools are: + set_summary, check_messages, send_message, check_team_status, get_plan, + update_plan, signal_done, submit_feedback, approve, list_peers, + acquire_file, release_file, view_file_locks, get_history + +BEFORE ANY OTHER WORK — call these MCP tools in this exact order: + 1. set_summary → describe your task in one line + 2. check_team_status → see your teammates and their roles + 3. get_plan → see the plan and your assigned items + 4. check_messages → read pending messages from teammates + +AFTER EVERY SHELL COMMAND OR FILE WRITE: + → Call check_messages (teammates may have sent critical updates) + → Call set_summary with what you just did + +AFTER EVERY 2-3 TOOL CALLS: + → Read your inbox file for real-time messages from teammates${inboxHint} + +WHEN YOU FINISH YOUR TASK: + → Call signal_done with what you built, tested, and results + → Then call check_messages every 10s waiting for review feedback + +WHEN A TEAMMATE MESSAGES YOU: + → Reply via send_message IMMEDIATELY — they are blocked waiting on you + +DO NOT: + ✗ Work for 1+ minute without calling check_messages + ✗ Finish without calling signal_done + ✗ Ignore teammate messages + ✗ Try to use CLI/HTTP to talk to the broker — use MCP tools only + ✗ Go silent — if you have nothing to do, call check_team_status and set_summary "Idle, ready to help" + +QUALITY: Production-grade code. Plan before coding. Verify before signaling done.`; + + if (this.roleContext) { + return `${base}\n\n--- ROLE CONTEXT ---\n${this.roleContext}`; + } + return base; + } + + /** + * Deliver a message by both buffering (for piggyback) and writing to + * the file-based inbox so Kimi sees it via native file reads. + */ + async deliverMessage(msg: BufferedMessage): Promise { + this.messageBuffer.push(msg); + await this.writeToInbox(msg); + } + + // --- File-based inbox --- + + private ensureInboxPath(): string | null { + if (this.inboxPath) return this.inboxPath; + + const slotId = this.mySlot?.id; + const name = this.mySlot?.display_name ?? (slotId ? `slot-${slotId}` : null); + if (!name) return null; + + const inboxDir = path.join(process.cwd(), ".multiagents", "inbox"); + try { + if (!fs.existsSync(inboxDir)) { + fs.mkdirSync(inboxDir, { recursive: true }); + } + } catch { + return null; + } + + const safeName = path.basename(name).replace(/[^a-zA-Z0-9_\-. ]/g, "_"); + this.inboxPath = path.join(inboxDir, `${safeName}.md`); + + try { + fs.writeFileSync(this.inboxPath, `# Inbox for ${name}\n\nMessages from teammates appear below. Newest at bottom.\n\n---\n\n`); + } catch { /* best effort */ } + + return this.inboxPath; + } + + private async writeToInbox(msg: BufferedMessage): Promise { + const inboxPath = this.ensureInboxPath(); + if (!inboxPath) return; + + try { + const timestamp = new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" }); + const from = msg.from_display_name ?? msg.from_role ?? msg.from_id; + const entry = `**[${timestamp}] ${from}** (${msg.msg_type}):\n${msg.text}\n\n---\n\n`; + fs.appendFileSync(inboxPath, entry); + } catch { /* best effort */ } + } + + // --- Auto-startup: inject team context on first MCP tool call --- + + private async runAutoStartup(): Promise { + if (this.hasRunStartup) return ""; + this.hasRunStartup = true; + + const parts: string[] = [ + "╔══════════════════════════════════════════════════════════╗", + "║ AUTO-STARTUP: Team context injected on first tool call ║", + "╚══════════════════════════════════════════════════════════╝", + "", + ]; + + try { + const role = this.mySlot?.role ?? "team member"; + const name = this.mySlot?.display_name ?? "agent"; + await this.broker.setSummary(this.myId!, `${name} (${role}) — starting up, reading team context`); + parts.push(`✓ set_summary: Registered as ${name} (${role})`); + } catch (e) { + parts.push(`✗ set_summary failed: ${e instanceof Error ? e.message : String(e)}`); + } + + try { + const teamResult = await this.handleCheckTeamStatus(); + parts.push("", "── TEAM STATUS ──", teamResult.content[0]?.text ?? ""); + } catch { parts.push("✗ check_team_status failed"); } + + try { + const planResult = await this.handleGetPlan(); + parts.push("", "── YOUR PLAN ──", planResult.content[0]?.text ?? ""); + } catch { parts.push("✗ get_plan failed"); } + + try { + const msgResult = await this.handleCheckMessages(); + parts.push("", "── PENDING MESSAGES ──", msgResult.content[0]?.text ?? ""); + } catch { parts.push("✗ check_messages failed"); } + + parts.push( + "", + "── ACTION REQUIRED ──", + "Use multiagents-peer MCP tools to communicate:", + " → set_summary after every major action", + " → check_messages after every shell command", + " → send_message to reply to teammates", + " → signal_done when finished (with proof)", + " → update_plan to mark items in_progress/done", + "DO NOT go 1+ minute without calling check_messages.", + "════════════════════════════════════════════════════════════", + "", + ); + + return parts.join("\n"); + } + + protected wrapToolResult(result: string): string { + if (this.messageBuffer.length === 0) { + return result; + } + + const pending = this.messageBuffer + .map((m) => this.formatMessage(m)) + .join("\n"); + this.messageBuffer = []; + + return `[PENDING MESSAGES]\n${pending}\n[END PENDING MESSAGES]\n\n${result}`; + } + + protected override async handleListPeers(args: any) { + const r = await super.handleListPeers(args); + return this.wrapResult(r); + } + + protected override async handleSendMessage(args: any) { + const r = await super.handleSendMessage(args); + return this.wrapResult(r); + } + + protected override async handleSetSummary(args: any) { + const r = await super.handleSetSummary(args); + return this.wrapResult(r); + } + + protected override async handleCheckMessages() { + const r = await super.handleCheckMessages(); + return this.wrapResult(r); + } + + protected override async handleAssignRole(args: any) { + const r = await super.handleAssignRole(args); + return this.wrapResult(r); + } + + protected override async handleRenamePeer(args: any) { + const r = await super.handleRenamePeer(args); + return this.wrapResult(r); + } + + protected override async handleAcquireFile(args: any) { + const r = await super.handleAcquireFile(args); + return this.wrapResult(r); + } + + protected override async handleReleaseFile(args: any) { + const r = await super.handleReleaseFile(args); + return this.wrapResult(r); + } + + protected override async handleViewFileLocks() { + const r = await super.handleViewFileLocks(); + return this.wrapResult(r); + } + + protected override async handleGetHistory(args: any) { + const r = await super.handleGetHistory(args); + return this.wrapResult(r); + } + + // --- Lifecycle tool wrappers (critical for message delivery) --- + + protected override async handleSignalDone(args: any) { + const r = await super.handleSignalDone(args); + return this.wrapResult(r); + } + + protected override async handleSubmitFeedback(args: any) { + const r = await super.handleSubmitFeedback(args); + return this.wrapResult(r); + } + + protected override async handleApprove(args: any) { + const r = await super.handleApprove(args); + return this.wrapResult(r); + } + + protected override async handleCheckTeamStatus() { + const r = await super.handleCheckTeamStatus(); + return this.wrapResult(r); + } + + protected override async handleGetPlan() { + const r = await super.handleGetPlan(); + return this.wrapResult(r); + } + + protected override async handleUpdatePlan(args: any) { + const r = await super.handleUpdatePlan(args); + return this.wrapResult(r); + } + + private async wrapResult(result: { content: { type: string; text: string }[]; isError?: boolean }) { + const text = result.content[0]?.text ?? ""; + const startupContext = await this.runAutoStartup(); + const wrapped = this.wrapToolResult(text); + return { + ...result, + content: [{ type: "text" as const, text: startupContext + wrapped }], + }; + } +} diff --git a/adapters/qwen-adapter.ts b/adapters/qwen-adapter.ts new file mode 100644 index 0000000..fbe1a8b --- /dev/null +++ b/adapters/qwen-adapter.ts @@ -0,0 +1,287 @@ +#!/usr/bin/env bun +/** + * QwenAdapter — MCP adapter for Qwen CLI instances. + * + * Nearly identical to CodexAdapter: no push capability. Messages are + * delivered via file-based inbox + piggybacked on tool responses. + */ + +import { BaseAdapter } from "./base-adapter.ts"; +import type { BufferedMessage } from "../shared/types.ts"; +import * as fs from "node:fs"; +import * as path from "node:path"; + +export class QwenAdapter extends BaseAdapter { + private messageBuffer: BufferedMessage[] = []; + private inboxPath: string | null = null; + private hasRunStartup = false; + + constructor() { + super("qwen"); + } + + getCapabilities(): Record { + return { + tools: {}, + }; + } + + getSystemPrompt(): string { + const inboxHint = this.inboxPath + ? `\nYour INBOX FILE: ${this.inboxPath}` + : "\nYour INBOX FILE: .multiagents/inbox/.md (created after registration)"; + + const base = `You are an Alibaba Qwen CLI instance (Qwen3 / Qwen2.5-coder) on the multiagents network. + +You are a team member on the multiagents network. You MUST use the multiagents-peer MCP tools to communicate. Your team CANNOT see your work unless you tell them. + +═══ COMMUNICATION IS YOUR #1 PRIORITY ═══ + +You have MCP tools from the "multiagents-peer" server. USE THEM — not shell commands, not HTTP calls, not CLI scripts. The MCP tools are: + set_summary, check_messages, send_message, check_team_status, get_plan, + update_plan, signal_done, submit_feedback, approve, list_peers, + acquire_file, release_file, view_file_locks, get_history + +BEFORE ANY OTHER WORK — call these MCP tools in this exact order: + 1. set_summary → describe your task in one line + 2. check_team_status → see your teammates and their roles + 3. get_plan → see the plan and your assigned items + 4. check_messages → read pending messages from teammates + +AFTER EVERY SHELL COMMAND OR FILE WRITE: + → Call check_messages (teammates may have sent critical updates) + → Call set_summary with what you just did + +AFTER EVERY 2-3 TOOL CALLS: + → Read your inbox file for real-time messages from teammates${inboxHint} + +WHEN YOU FINISH YOUR TASK: + → Call signal_done with what you built, tested, and results + → Then call check_messages every 10s waiting for review feedback + +WHEN A TEAMMATE MESSAGES YOU: + → Reply via send_message IMMEDIATELY — they are blocked waiting on you + +DO NOT: + ✗ Work for 1+ minute without calling check_messages + ✗ Finish without calling signal_done + ✗ Ignore teammate messages + ✗ Try to use CLI/HTTP to talk to the broker — use MCP tools only + ✗ Go silent — if you have nothing to do, call check_team_status and set_summary "Idle, ready to help" + +QUALITY: Production-grade code. Plan before coding. Verify before signaling done.`; + + if (this.roleContext) { + return `${base}\n\n--- ROLE CONTEXT ---\n${this.roleContext}`; + } + return base; + } + + /** + * Deliver a message by both buffering (for piggyback) and writing to + * the file-based inbox so Qwen sees it via native file reads. + */ + async deliverMessage(msg: BufferedMessage): Promise { + this.messageBuffer.push(msg); + await this.writeToInbox(msg); + } + + // --- File-based inbox --- + + private ensureInboxPath(): string | null { + if (this.inboxPath) return this.inboxPath; + + const slotId = this.mySlot?.id; + const name = this.mySlot?.display_name ?? (slotId ? `slot-${slotId}` : null); + if (!name) return null; + + const inboxDir = path.join(process.cwd(), ".multiagents", "inbox"); + try { + if (!fs.existsSync(inboxDir)) { + fs.mkdirSync(inboxDir, { recursive: true }); + } + } catch { + return null; + } + + const safeName = path.basename(name).replace(/[^a-zA-Z0-9_\-. ]/g, "_"); + this.inboxPath = path.join(inboxDir, `${safeName}.md`); + + try { + fs.writeFileSync(this.inboxPath, `# Inbox for ${name}\n\nMessages from teammates appear below. Newest at bottom.\n\n---\n\n`); + } catch { /* best effort */ } + + return this.inboxPath; + } + + private async writeToInbox(msg: BufferedMessage): Promise { + const inboxPath = this.ensureInboxPath(); + if (!inboxPath) return; + + try { + const timestamp = new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" }); + const from = msg.from_display_name ?? msg.from_role ?? msg.from_id; + const entry = `**[${timestamp}] ${from}** (${msg.msg_type}):\n${msg.text}\n\n---\n\n`; + fs.appendFileSync(inboxPath, entry); + } catch { /* best effort */ } + } + + // --- Auto-startup: inject team context on first MCP tool call --- + + private async runAutoStartup(): Promise { + if (this.hasRunStartup) return ""; + this.hasRunStartup = true; + + const parts: string[] = [ + "╔══════════════════════════════════════════════════════════╗", + "║ AUTO-STARTUP: Team context injected on first tool call ║", + "╚══════════════════════════════════════════════════════════╝", + "", + ]; + + try { + const role = this.mySlot?.role ?? "team member"; + const name = this.mySlot?.display_name ?? "agent"; + await this.broker.setSummary(this.myId!, `${name} (${role}) — starting up, reading team context`); + parts.push(`✓ set_summary: Registered as ${name} (${role})`); + } catch (e) { + parts.push(`✗ set_summary failed: ${e instanceof Error ? e.message : String(e)}`); + } + + try { + const teamResult = await this.handleCheckTeamStatus(); + parts.push("", "── TEAM STATUS ──", teamResult.content[0]?.text ?? ""); + } catch { parts.push("✗ check_team_status failed"); } + + try { + const planResult = await this.handleGetPlan(); + parts.push("", "── YOUR PLAN ──", planResult.content[0]?.text ?? ""); + } catch { parts.push("✗ get_plan failed"); } + + try { + const msgResult = await this.handleCheckMessages(); + parts.push("", "── PENDING MESSAGES ──", msgResult.content[0]?.text ?? ""); + } catch { parts.push("✗ check_messages failed"); } + + parts.push( + "", + "── ACTION REQUIRED ──", + "Use multiagents-peer MCP tools to communicate:", + " → set_summary after every major action", + " → check_messages after every shell command", + " → send_message to reply to teammates", + " → signal_done when finished (with proof)", + " → update_plan to mark items in_progress/done", + "DO NOT go 1+ minute without calling check_messages.", + "════════════════════════════════════════════════════════════", + "", + ); + + return parts.join("\n"); + } + + protected wrapToolResult(result: string): string { + if (this.messageBuffer.length === 0) { + return result; + } + + const pending = this.messageBuffer + .map((m) => this.formatMessage(m)) + .join("\n"); + this.messageBuffer = []; + + return `[PENDING MESSAGES]\n${pending}\n[END PENDING MESSAGES]\n\n${result}`; + } + + protected override async handleListPeers(args: any) { + const r = await super.handleListPeers(args); + return this.wrapResult(r); + } + + protected override async handleSendMessage(args: any) { + const r = await super.handleSendMessage(args); + return this.wrapResult(r); + } + + protected override async handleSetSummary(args: any) { + const r = await super.handleSetSummary(args); + return this.wrapResult(r); + } + + protected override async handleCheckMessages() { + const r = await super.handleCheckMessages(); + return this.wrapResult(r); + } + + protected override async handleAssignRole(args: any) { + const r = await super.handleAssignRole(args); + return this.wrapResult(r); + } + + protected override async handleRenamePeer(args: any) { + const r = await super.handleRenamePeer(args); + return this.wrapResult(r); + } + + protected override async handleAcquireFile(args: any) { + const r = await super.handleAcquireFile(args); + return this.wrapResult(r); + } + + protected override async handleReleaseFile(args: any) { + const r = await super.handleReleaseFile(args); + return this.wrapResult(r); + } + + protected override async handleViewFileLocks() { + const r = await super.handleViewFileLocks(); + return this.wrapResult(r); + } + + protected override async handleGetHistory(args: any) { + const r = await super.handleGetHistory(args); + return this.wrapResult(r); + } + + // --- Lifecycle tool wrappers (critical for message delivery) --- + + protected override async handleSignalDone(args: any) { + const r = await super.handleSignalDone(args); + return this.wrapResult(r); + } + + protected override async handleSubmitFeedback(args: any) { + const r = await super.handleSubmitFeedback(args); + return this.wrapResult(r); + } + + protected override async handleApprove(args: any) { + const r = await super.handleApprove(args); + return this.wrapResult(r); + } + + protected override async handleCheckTeamStatus() { + const r = await super.handleCheckTeamStatus(); + return this.wrapResult(r); + } + + protected override async handleGetPlan() { + const r = await super.handleGetPlan(); + return this.wrapResult(r); + } + + protected override async handleUpdatePlan(args: any) { + const r = await super.handleUpdatePlan(args); + return this.wrapResult(r); + } + + private async wrapResult(result: { content: { type: string; text: string }[]; isError?: boolean }) { + const text = result.content[0]?.text ?? ""; + const startupContext = await this.runAutoStartup(); + const wrapped = this.wrapToolResult(text); + return { + ...result, + content: [{ type: "text" as const, text: startupContext + wrapped }], + }; + } +} diff --git a/server.ts b/server.ts index 9dbffd3..5a36565 100755 --- a/server.ts +++ b/server.ts @@ -8,6 +8,10 @@ * bun server.ts # defaults to claude * bun server.ts --agent-type codex * bun server.ts --agent-type gemini + * bun server.ts --agent-type kimi + * bun server.ts --agent-type copilot + * bun server.ts --agent-type qwen + * bun server.ts --agent-type jinn */ import type { AgentType } from "./shared/types.ts"; @@ -35,6 +39,26 @@ async function main() { await new GeminiAdapter().start(); break; } + case "kimi": { + const { KimiAdapter } = await import("./adapters/kimi-adapter.ts"); + await new KimiAdapter().start(); + break; + } + case "copilot": { + const { CopilotAdapter } = await import("./adapters/copilot-adapter.ts"); + await new CopilotAdapter().start(); + break; + } + case "qwen": { + const { QwenAdapter } = await import("./adapters/qwen-adapter.ts"); + await new QwenAdapter().start(); + break; + } + case "jinn": { + const { JinnAdapter } = await import("./adapters/jinn-adapter.ts"); + await new JinnAdapter().start(); + break; + } default: { // For 'custom' or unknown types, fall back to Claude adapter behavior const { ClaudeAdapter } = await import("./adapters/claude-adapter.ts"); @@ -45,8 +69,6 @@ async function main() { } main().catch((e) => { - console.error( - `[multiagents] Fatal: ${e instanceof Error ? e.message : String(e)}`, - ); + console.error(`[multiagents] Fatal: ${e instanceof Error ? e.message : String(e)}`); process.exit(1); }); diff --git a/shared/constants.ts b/shared/constants.ts index 9679075..ea0c0c6 100644 --- a/shared/constants.ts +++ b/shared/constants.ts @@ -16,6 +16,10 @@ export const POLL_INTERVALS: Record = { claude: 1000, // Claude uses channel push — polling is fallback only codex: 300, // Aggressive: piggyback delivery depends on fast buffering gemini: 300, // Same as Codex — no push capability + kimi: 500, // Kimi K2.6 — push capability to be tested, fallback polling + copilot: 300, // Copilot CLI — no push, aggressive polling for piggyback + qwen: 300, // Qwen CLI — same as Gemini, no push + jinn: 1000, // Jinn bridge — gateway API bridge, moderate polling custom: 500, // Reasonable default for unknown agents }; @@ -46,6 +50,10 @@ export const AGENT_ID_PREFIXES: Record = { claude: "cl", codex: "cx", gemini: "gm", + kimi: "km", + copilot: "cp", + qwen: "qw", + jinn: "jn", custom: "cu", }; diff --git a/shared/types.ts b/shared/types.ts index c1d8736..bfbed8d 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -10,7 +10,7 @@ export type PeerId = string; /** Supported agent CLI types */ -export type AgentType = "claude" | "codex" | "gemini" | "custom"; +export type AgentType = "claude" | "codex" | "gemini" | "kimi" | "copilot" | "qwen" | "jinn" | "custom"; /** Message types for routing and formatting */ export type MessageType = diff --git a/tests/homelab-adapters.test.ts b/tests/homelab-adapters.test.ts new file mode 100644 index 0000000..2516340 --- /dev/null +++ b/tests/homelab-adapters.test.ts @@ -0,0 +1,88 @@ +import { test, expect, describe, beforeAll, afterAll } from "bun:test"; +import { spawn, type Subprocess } from "bun"; +import * as path from "node:path"; + +const ROOT = path.resolve(import.meta.dir, ".."); +const TEST_PORT = "7901"; // éviter conflit avec brokers dev/prod + +let broker: Subprocess; + +beforeAll(async () => { + broker = spawn({ + cmd: ["bun", `${ROOT}/broker.ts`], + env: { ...process.env, MULTIAGENTS_PORT: TEST_PORT }, + stdout: "pipe", + stderr: "pipe", + }); + // attendre que le broker bind le port + for (let i = 0; i < 30; i++) { + try { + const r = await fetch(`http://127.0.0.1:${TEST_PORT}/health`); + if (r.ok) return; + } catch {} + await Bun.sleep(100); + } + throw new Error("broker did not start within 3s"); +}); + +afterAll(() => { + broker?.kill(); +}); + +describe("homelab adapters register with correct prefix", () => { + const cases: Array<{ type: string; prefix: string; className: string }> = [ + { type: "kimi", prefix: "km-", className: "KimiAdapter" }, + { type: "copilot", prefix: "cp-", className: "CopilotAdapter" }, + { type: "qwen", prefix: "qw-", className: "QwenAdapter" }, + { type: "jinn", prefix: "jn-", className: "JinnAdapter" }, + ]; + + for (const { type, prefix, className } of cases) { + test(`${type} → ${className} registers with ${prefix} prefix`, async () => { + const adapter = spawn({ + cmd: ["bun", `${ROOT}/server.ts`, "--agent-type", type], + env: { ...process.env, MULTIAGENTS_PORT: TEST_PORT }, + stdin: "pipe", + stdout: "pipe", + stderr: "pipe", + }); + + // Send MCP initialize to unblock the handshake + adapter.stdin.write(JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "2024-11-05", + capabilities: {}, + clientInfo: { name: "test-probe", version: "1" }, + }, + }) + "\n"); + adapter.stdin.flush(); + + // Give the adapter time to register with the broker + await Bun.sleep(2500); + + // Read stderr to capture the registration line + const reader = adapter.stderr.getReader(); + const chunks: string[] = []; + const deadline = Date.now() + 500; + while (Date.now() < deadline) { + const race = await Promise.race([ + reader.read(), + new Promise((res) => setTimeout(() => res({ done: true, value: null }), 100)), + ]); + if (race.done || !race.value) break; + chunks.push(new TextDecoder().decode(race.value)); + } + const stderrText = chunks.join(""); + + adapter.kill(); + await Bun.sleep(200); + + const match = stderrText.match(/Registered as peer (\S+)/); + expect(match).not.toBeNull(); + expect(match![1]).toMatch(new RegExp(`^${prefix}`)); + }, 10000); // timeout 10s + } +});