UNPKG

consortium

Version:

Remote control and session sharing CLI for AI coding agents

402 lines (395 loc) 12.2 kB
import { l as logger, p as projectPath, c as configuration, b as packageJson, s as startOfflineReconnection } from './types-WAGIe5yd.mjs'; import { randomUUID } from 'node:crypto'; import os from 'node:os'; import { resolve } from 'node:path'; class BasePermissionHandler { pendingRequests = /* @__PURE__ */ new Map(); session; isResetting = false; constructor(session) { this.session = session; this.setupRpcHandler(); } /** * Update the session reference (used after offline reconnection swaps sessions). * This is critical for avoiding stale session references after onSessionSwap. */ updateSession(newSession) { logger.debug(`${this.getLogPrefix()} Session reference updated`); this.session = newSession; this.setupRpcHandler(); } /** * Setup RPC handler for permission responses. */ setupRpcHandler() { this.session.rpcHandlerManager.registerHandler( "permission", async (response) => { const pending = this.pendingRequests.get(response.id); if (!pending) { logger.debug(`${this.getLogPrefix()} Permission request not found or already resolved`); return; } this.pendingRequests.delete(response.id); const result = response.approved ? { decision: response.decision === "approved_for_session" ? "approved_for_session" : "approved" } : { decision: response.decision === "denied" ? "denied" : "abort" }; pending.resolve(result); this.session.updateAgentState((currentState) => { const request = currentState.requests?.[response.id]; if (!request) return currentState; const { [response.id]: _, ...remainingRequests } = currentState.requests || {}; let res = { ...currentState, requests: remainingRequests, completedRequests: { ...currentState.completedRequests, [response.id]: { ...request, completedAt: Date.now(), status: response.approved ? "approved" : "denied", decision: result.decision } } }; return res; }); logger.debug(`${this.getLogPrefix()} Permission ${response.approved ? "approved" : "denied"} for ${pending.toolName}`); } ); } /** * Add a pending request to the agent state. */ addPendingRequestToState(toolCallId, toolName, input) { this.session.updateAgentState((currentState) => ({ ...currentState, requests: { ...currentState.requests, [toolCallId]: { tool: toolName, arguments: input, createdAt: Date.now() } } })); } /** * Reset state for new sessions. * This method is idempotent - safe to call multiple times. */ reset() { if (this.isResetting) { logger.debug(`${this.getLogPrefix()} Reset already in progress, skipping`); return; } this.isResetting = true; try { const pendingSnapshot = Array.from(this.pendingRequests.entries()); this.pendingRequests.clear(); for (const [id, pending] of pendingSnapshot) { try { pending.reject(new Error("Session reset")); } catch (err) { logger.debug(`${this.getLogPrefix()} Error rejecting pending request ${id}:`, err); } } this.session.updateAgentState((currentState) => { const pendingRequests = currentState.requests || {}; const completedRequests = { ...currentState.completedRequests }; for (const [id, request] of Object.entries(pendingRequests)) { completedRequests[id] = { ...request, completedAt: Date.now(), status: "canceled", reason: "Session reset" }; } return { ...currentState, requests: {}, completedRequests }; }); logger.debug(`${this.getLogPrefix()} Permission handler reset`); } finally { this.isResetting = false; } } } class BaseReasoningProcessor { accumulator = ""; inTitleCapture = false; titleBuffer = ""; contentBuffer = ""; hasTitle = false; currentCallId = null; toolCallStarted = false; currentTitle = null; onMessage = null; constructor(onMessage) { this.onMessage = onMessage || null; this.reset(); } /** * Set the message callback for sending messages directly. */ setMessageCallback(callback) { this.onMessage = callback; } /** * Process a reasoning section break - indicates a new reasoning section is starting. */ handleSectionBreak() { this.finishCurrentToolCall("canceled"); this.resetState(); logger.debug(`${this.getLogPrefix()} Section break - reset state`); } /** * Process a reasoning delta/chunk and accumulate content. */ processInput(input) { this.accumulator += input; if (!this.inTitleCapture && !this.hasTitle && !this.contentBuffer) { if (this.accumulator.startsWith("**")) { this.inTitleCapture = true; this.titleBuffer = this.accumulator.substring(2); logger.debug(`${this.getLogPrefix()} Started title capture`); } else if (this.accumulator.length > 0) { this.contentBuffer = this.accumulator; } } else if (this.inTitleCapture) { this.titleBuffer = this.accumulator.substring(2); const titleEndIndex = this.titleBuffer.indexOf("**"); if (titleEndIndex !== -1) { const title = this.titleBuffer.substring(0, titleEndIndex); const afterTitle = this.titleBuffer.substring(titleEndIndex + 2); this.hasTitle = true; this.inTitleCapture = false; this.currentTitle = title; this.contentBuffer = afterTitle; this.currentCallId = randomUUID(); logger.debug(`${this.getLogPrefix()} Title captured: "${title}"`); this.sendToolCallStart(title); } } else if (this.hasTitle) { const titleStartIndex = this.accumulator.indexOf("**"); if (titleStartIndex !== -1) { this.contentBuffer = this.accumulator.substring( titleStartIndex + 2 + this.currentTitle.length + 2 ); } } else { this.contentBuffer = this.accumulator; } } /** * Send the tool call start message. */ sendToolCallStart(title) { if (!this.currentCallId || this.toolCallStarted) { return; } const toolCall = { type: "tool-call", name: this.getToolName(), callId: this.currentCallId, input: { title }, id: randomUUID() }; logger.debug(`${this.getLogPrefix()} Sending tool call start for: "${title}"`); this.onMessage?.(toolCall); this.toolCallStarted = true; } /** * Complete the reasoning section. * Returns true if reasoning was completed, false if there was nothing to complete. */ completeReasoning(fullText) { const text = fullText ?? this.accumulator; if (!text.trim() && !this.toolCallStarted) { logger.debug(`${this.getLogPrefix()} Complete called but no content accumulated, skipping`); return false; } let title; let content = text; if (text.startsWith("**")) { const titleEndIndex = text.indexOf("**", 2); if (titleEndIndex !== -1) { title = text.substring(2, titleEndIndex); content = text.substring(titleEndIndex + 2).trim(); } } logger.debug(`${this.getLogPrefix()} Complete reasoning - Title: "${title}", Has content: ${content.length > 0}`); if (title && !this.toolCallStarted) { this.currentCallId = this.currentCallId || randomUUID(); this.sendToolCallStart(title); } if (this.toolCallStarted && this.currentCallId) { const toolResult = { type: "tool-call-result", callId: this.currentCallId, output: { content, status: "completed" }, id: randomUUID() }; logger.debug(`${this.getLogPrefix()} Sending tool call result`); this.onMessage?.(toolResult); } else if (content.trim()) { const reasoningMessage = { type: "reasoning", message: content, id: randomUUID() }; logger.debug(`${this.getLogPrefix()} Sending reasoning message`); this.onMessage?.(reasoningMessage); } this.resetState(); return true; } /** * Abort the current reasoning section. */ abort() { logger.debug(`${this.getLogPrefix()} Abort called`); this.finishCurrentToolCall("canceled"); this.resetState(); } /** * Reset the processor state. */ reset() { this.finishCurrentToolCall("canceled"); this.resetState(); } /** * Finish current tool call if one is in progress. */ finishCurrentToolCall(status) { if (this.toolCallStarted && this.currentCallId) { const toolResult = { type: "tool-call-result", callId: this.currentCallId, output: { content: this.contentBuffer || "", status }, id: randomUUID() }; logger.debug(`${this.getLogPrefix()} Sending tool call result with status: ${status}`); this.onMessage?.(toolResult); } } /** * Reset internal state. */ resetState() { this.accumulator = ""; this.inTitleCapture = false; this.titleBuffer = ""; this.contentBuffer = ""; this.hasTitle = false; this.currentCallId = null; this.toolCallStarted = false; this.currentTitle = null; } /** * Get the current call ID for tool result matching. */ getCurrentCallId() { return this.currentCallId; } /** * Check if a tool call has been started. */ hasStartedToolCall() { return this.toolCallStarted; } } function createSessionMetadata(opts) { const state = { controlledByUser: false }; const metadata = { path: process.cwd(), host: os.hostname(), version: packageJson.version, os: os.platform(), machineId: opts.machineId, homeDir: os.homedir(), consortiumHomeDir: configuration.consortiumHomeDir, consortiumLibDir: projectPath(), consortiumToolsDir: resolve(projectPath(), "tools", "unpacked"), startedFromDaemon: opts.startedBy === "daemon", hostPid: process.pid, startedBy: opts.startedBy || "terminal", lifecycleState: "running", lifecycleStateSince: Date.now(), flavor: opts.flavor }; return { state, metadata }; } function createOfflineSessionStub(sessionTag) { return { sessionId: `offline-${sessionTag}`, sendCodexMessage: () => { }, sendAgentMessage: () => { }, sendClaudeSessionMessage: () => { }, keepAlive: () => { }, sendSessionEvent: () => { }, sendSessionDeath: () => { }, updateLifecycleState: () => { }, requestControlTransfer: async () => { }, flush: async () => { }, close: async () => { }, updateMetadata: () => { }, updateAgentState: () => { }, onUserMessage: () => { }, rpcHandlerManager: { registerHandler: () => { } } }; } function setupOfflineReconnection(opts) { const { api, sessionTag, metadata, state, response, onSessionSwap } = opts; let session; let reconnectionHandle = null; if (!response) { session = createOfflineSessionStub(sessionTag); reconnectionHandle = startOfflineReconnection({ serverUrl: configuration.serverUrl, onReconnected: async () => { const resp = await api.getOrCreateSession({ tag: sessionTag, metadata, state }); if (!resp) throw new Error("Server unavailable"); const realSession = api.sessionSyncClient(resp); onSessionSwap(realSession); return realSession; }, onNotify: (msg) => { console.log(msg); } }); return { session, reconnectionHandle, isOffline: true }; } else { session = api.sessionSyncClient(response); return { session, reconnectionHandle: null, isOffline: false }; } } export { BasePermissionHandler as B, BaseReasoningProcessor as a, createSessionMetadata as c, setupOfflineReconnection as s };