@hera-al/server 1.6.41 → 1.6.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/agent-service.js +1 -1
- package/dist/agent/prompt-builder.d.ts +1 -1
- package/dist/agent/prompt-builder.js +1 -1
- package/dist/agent/session-agent.d.ts +1 -1
- package/dist/agent/session-agent.js +1 -1
- package/dist/agent/workspace-files.js +1 -1
- package/dist/config.d.ts +21 -0
- package/dist/config.js +1 -1
- package/dist/gateway/bridge.d.ts +13 -3
- package/dist/gateway/channel-manager.d.ts +3 -9
- package/dist/gateway/channel-manager.js +1 -1
- package/dist/gateway/channels/mesh.d.ts +14 -0
- package/dist/gateway/channels/mesh.js +1 -1
- package/dist/gateway/channels/telegram/index.d.ts +22 -12
- package/dist/gateway/channels/telegram/index.js +1 -1
- package/dist/gateway/channels/webchat.d.ts +3 -6
- package/dist/gateway/channels/webchat.js +1 -1
- package/dist/gateway/channels/whatsapp.d.ts +3 -9
- package/dist/gateway/channels/whatsapp.js +1 -1
- package/dist/gateway/typing-coordinator.d.ts +43 -0
- package/dist/gateway/typing-coordinator.js +1 -0
- package/dist/memory/concept-store.d.ts +27 -0
- package/dist/memory/concept-store.js +1 -1
- package/dist/memory/memory-manager.d.ts +6 -1
- package/dist/memory/memory-manager.js +1 -1
- package/dist/memory/memory-search.d.ts +63 -0
- package/dist/memory/memory-search.js +1 -1
- package/dist/server.d.ts +3 -0
- package/dist/server.js +1 -1
- package/dist/tools/cron-tools.js +1 -1
- package/dist/tools/memory-tools.js +1 -1
- package/dist/tools/operational-context-tools.d.ts +20 -0
- package/dist/tools/operational-context-tools.js +1 -0
- package/dist/tools/server-tools.d.ts +11 -1
- package/dist/tools/server-tools.js +1 -1
- package/installationPkg/AGENTS.md +4 -0
- package/installationPkg/BEHAVIOUR.md +95 -76
- package/installationPkg/config.example.yaml +31 -3
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{query as s}from"@anthropic-ai/claude-agent-sdk";import{SessionAgent as o}from"./session-agent.js";import{createNodeToolsServer as t}from"../tools/node-tools.js";import{createMessageToolsServer as e}from"../tools/message-tools.js";import{createTelegramActionsToolsServer as r}from"../tools/telegram-actions-tools.js";import{createA2UIToolsServer as i}from"../tools/a2ui-tools.js";import{createDynamicUIToolsServer as n}from"../tools/dynamic-ui-tools.js";import{createStateToolsServer as a}from"../tools/state-tools.js";import{
|
|
1
|
+
import{query as s}from"@anthropic-ai/claude-agent-sdk";import{SessionAgent as o}from"./session-agent.js";import{createNodeToolsServer as t}from"../tools/node-tools.js";import{createMessageToolsServer as e}from"../tools/message-tools.js";import{createTelegramActionsToolsServer as r}from"../tools/telegram-actions-tools.js";import{createA2UIToolsServer as i}from"../tools/a2ui-tools.js";import{createDynamicUIToolsServer as n}from"../tools/dynamic-ui-tools.js";import{createStateToolsServer as a}from"../tools/state-tools.js";import{createOperationalContextToolsServer as l}from"../tools/operational-context-tools.js";import{createLogger as c}from"../utils/logger.js";const h=c("AgentService");export class AgentService{config;agents=new Map;usageBySession=new Map;nodeToolsFactory=null;messageToolsFactory=null;serverToolsFactory=null;cronToolsFactory=null;ttsToolsFactory=null;memoryToolsFactory=null;browserToolsFactory=null;picoToolsFactory=null;telegramToolsFactory=null;a2uiToolsFactory=null;dynamicUIToolsFactory=null;plasmaClientToolsFactory=null;conceptToolsFactory=null;refToolServers=null;channelManager=null;nodeRegistry=null;showToolUseGetter=null;constructor(s,o,a,l,c,h,g,y,d,u,T,m,F){this.config=s,a&&(this.channelManager=a,this.telegramToolsFactory=()=>r(a,()=>this.config)),o&&(this.nodeRegistry=o,this.nodeToolsFactory=()=>t(o),this.a2uiToolsFactory=()=>i({nodeRegistry:o}),this.dynamicUIToolsFactory=()=>n({nodeRegistry:o})),a&&h&&(this.messageToolsFactory=s=>e(a,()=>this.config,h,s)),l&&(this.serverToolsFactory=l),c&&(this.cronToolsFactory=c),g&&(this.ttsToolsFactory=g),y&&(this.memoryToolsFactory=y),u&&(this.browserToolsFactory=u),T&&(this.picoToolsFactory=T),m&&(this.plasmaClientToolsFactory=m),F&&(this.conceptToolsFactory=F),d&&(this.showToolUseGetter=d)}async sendMessage(s,o,t,e,r,i,n,a,l,c){const g=this.getOrCreateAgent(s,t,e,r,i,n,a,l,c);if(i&&i!==g.getModel()){const t=s=>{const o=this.config.models.find(o=>o.id===s);return!(!o?.proxy||"not-used"===o.proxy)},y=t(g.getModel()),d=t(i);if(y||d){h.info(`[${s}] Proxy config change detected (old=${y}, new=${d}), restarting session`),this.destroySession(s);const t=this.getOrCreateAgent(s,void 0,e,r,i,n,a,l,c);return await t.send(o)}await g.setModel(i)}try{return await g.send(o)}catch(y){const d=y instanceof Error?y.message:String(y);if(d.includes("INVALID_ARGUMENT")||/API Error: 400/.test(d)){h.warn(`[${s}] Transient API 400 error, retrying once: ${d.slice(0,120)}`),this.agents.delete(s),g.close();const y=this.getOrCreateAgent(s,t,e,r,i,n,a,l,c);try{return await y.send(o)}catch(o){h.error(`[${s}] Retry also failed: ${o}`),this.agents.delete(s),y.close();const e=o instanceof Error?o.message:String(o);if(t)return{response:e.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw o}}if(this.agents.delete(s),g.close(),t)return h.warn(`Session agent failed for ${s}: ${y}`),{response:d.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw y}}hasNodeTools(){return null!==this.nodeToolsFactory}hasMessageTools(){return null!==this.messageToolsFactory}getToolServers(){if(!this.refToolServers){const s=[];this.nodeToolsFactory&&s.push(this.nodeToolsFactory()),this.messageToolsFactory&&s.push(this.messageToolsFactory()),this.serverToolsFactory&&s.push(this.serverToolsFactory()),this.cronToolsFactory&&s.push(this.cronToolsFactory()),this.ttsToolsFactory&&s.push(this.ttsToolsFactory()),this.memoryToolsFactory&&s.push(this.memoryToolsFactory()),this.browserToolsFactory&&s.push(this.browserToolsFactory()),this.picoToolsFactory&&s.push(this.picoToolsFactory()),this.telegramToolsFactory&&s.push(this.telegramToolsFactory()),this.a2uiToolsFactory&&s.push(this.a2uiToolsFactory()),this.dynamicUIToolsFactory&&s.push(this.dynamicUIToolsFactory()),this.plasmaClientToolsFactory&&s.push(this.plasmaClientToolsFactory()),this.conceptToolsFactory&&s.push(this.conceptToolsFactory()),s.push(a("__ref__",this.config.dataDir)),this.refToolServers=s}return this.refToolServers}getOrCreateAgent(s,t,e,r,i,c,h,g,y){const d=this.agents.get(s);if(d&&d.isActive())return d;let u,T;d&&(d.close(),this.agents.delete(s));const m=s.indexOf(":"),F=m>0?s.substring(0,m):void 0,p=m>0?s.substring(m+1):void 0;this.nodeRegistry&&(F&&p?u=n({nodeRegistry:this.nodeRegistry,channel:F,chatId:p}):this.dynamicUIToolsFactory&&(u=this.dynamicUIToolsFactory())),this.plasmaClientToolsFactory&&(T=this.plasmaClientToolsFactory(F,p));const f=a(s,this.config.dataDir),v=l(this.config.dataDir),S=new o(s,this.config,e,r,t,i,this.nodeToolsFactory?.()??void 0,this.messageToolsFactory?.(s)??void 0,this.serverToolsFactory?.()??void 0,this.cronToolsFactory?.()??void 0,c,h,g,this.ttsToolsFactory?.()??void 0,this.memoryToolsFactory?.()??void 0,this.browserToolsFactory?.()??void 0,y,this.picoToolsFactory?.()??void 0,this.telegramToolsFactory?.()??void 0,this.a2uiToolsFactory?.()??void 0,u??void 0,T??void 0,this.conceptToolsFactory?.()??void 0,f,v);if(this.channelManager){const s=this.channelManager;if(S.setChannelSender(async(o,t,e,r)=>{r&&r.length>0?await s.sendButtons(o,t,e,[r]):await s.sendToChannel(o,t,e)}),this.showToolUseGetter){const o=this.showToolUseGetter;S.setToolUseNotifier(async(t,e,r)=>{if(!o(`${t}:${e}`))return;const i=`⚙️ Using ${r.replace(/^mcp__[^_]+__/,"")}`;await s.sendToChannel(t,e,i),await s.setTyping(t,e)})}S.setTypingSetter(async(o,t)=>{await s.setTyping(o,t)}),S.setTypingClearer(async(o,t)=>{await s.clearTyping(o,t)}),S.setTextBlockStreamer(async(o,t,e)=>{await s.sendResponse(o,t,e)})}return S.setUsageRecorder((s,o,t,e,r)=>{this.usageBySession.set(s,{totalCostUsd:o,durationMs:t,numTurns:e,modelUsage:r,recordedAt:Date.now()})}),this.agents.set(s,S),S}async interrupt(s){const o=this.agents.get(s);return!!o&&o.interrupt()}isBusy(s){const o=this.agents.get(s);return!!o&&o.isBusy()}hasPendingPermission(s){const o=this.agents.get(s);return!!o&&o.hasPendingPermission()}isFallbackActive(s){const o=this.agents.get(s);return!!o&&o.isFallbackActive()}resolvePermission(s,o){const t=this.agents.get(s);t&&t.resolvePermission(o)}hasPendingQuestion(s){const o=this.agents.get(s);return!!o&&o.hasPendingQuestion()}resolveQuestion(s,o){const t=this.agents.get(s);t&&t.resolveQuestion(o)}destroySession(s){const o=this.agents.get(s);o&&(o.close(),this.agents.delete(s),h.info(`Session agent destroyed: ${s}`))}destroyAll(){for(const[s,o]of this.agents)o.close(),h.info(`Session agent destroyed (reconfigure): ${s}`);this.agents.clear()}getActiveSessions(){return Array.from(this.agents.keys()).filter(s=>{const o=this.agents.get(s);return o&&o.isActive()})}getActiveSessionCount(){return this.getActiveSessions().length}getUsage(s){return this.usageBySession.get(s)}getSdkSlashCommands(){for(const s of this.agents.values()){const o=s.getSdkSlashCommands();if(o.length>0)return o}return[]}async listModels(){try{const o=s({prompt:"list models",options:{maxTurns:0}}),t=await o.supportedModels();for await(const s of o)break;return t.map(s=>({id:s.id??s.name??String(s),name:s.name??s.id??String(s)}))}catch(s){return h.error(`Failed to list models: ${s}`),[{id:"claude-sonnet-4-6",name:"Claude Sonnet 4.6"},{id:"claude-opus-4-6",name:"Claude Opus 4.6"},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku 3.5"}]}}}
|
|
@@ -62,5 +62,5 @@ export declare function resolvePlaceholders(template: string, vars: Record<strin
|
|
|
62
62
|
*
|
|
63
63
|
* Note: session_info is no longer included here — it's in the system prompt.
|
|
64
64
|
*/
|
|
65
|
-
export declare function buildPrompt(processed: ProcessedMessage, memoryContext?: string, sessionMeta?: SessionMeta, timezone?: string): BuiltPrompt;
|
|
65
|
+
export declare function buildPrompt(processed: ProcessedMessage, memoryContext?: string, sessionMeta?: SessionMeta, timezone?: string, l2Prefix?: string): BuiltPrompt;
|
|
66
66
|
//# sourceMappingURL=prompt-builder.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{hostname as e,type as n,release as t,arch as o}from"node:os";import{modelRefName as s}from"../config.js";import{loadTemplate as a,loadBuiltInTools as r,formatWorkspaceFiles as i,formatWorkspaceFilesWithMeta as l,filterForSubagent as c}from"./workspace-files.js";import{createLogger as m}from"../utils/logger.js";const d=m("PromptBuilder");export function buildSystemPrompt(l){const{config:m,sessionContext:f,mode:E,hasNodeTools:y,hasMessageTools:I,coderSkill:_,subagentTask:O,toolServers:A}=l,w="minimal"===E?"SYSTEM_PROMPT_SUBAGENT.md":"SYSTEM_PROMPT.md",
|
|
1
|
+
import{hostname as e,type as n,release as t,arch as o}from"node:os";import{modelRefName as s}from"../config.js";import{loadTemplate as a,loadBuiltInTools as r,formatWorkspaceFiles as i,formatWorkspaceFilesWithMeta as l,filterForSubagent as c}from"./workspace-files.js";import{createLogger as m}from"../utils/logger.js";const d=m("PromptBuilder");export function buildSystemPrompt(l){const{config:m,sessionContext:f,mode:E,hasNodeTools:y,hasMessageTools:I,coderSkill:_,subagentTask:O,toolServers:A}=l,w="minimal"===E?"SYSTEM_PROMPT_SUBAGENT.md":"SYSTEM_PROMPT.md",R=a(m.dataDir,w),N="minimal"===E?c(l.workspaceFiles):l.workspaceFiles;var b;const M=resolvePlaceholders(R,{SESSION_KEY:f.sessionKey,CHANNEL:f.channel,CHAT_ID:f.chatId,SESSION_ID:f.sessionId||"(new session)",MODEL:s(m.agent.model),HOSTNAME:e(),OS:`${n()} ${t()} (${o()})`,WORKSPACE_DIR:m.agent.workspacePath,DATA_DIR:m.dataDir,MEMORY_FILE:f.memoryFile||"(memory disabled)",ATTACHMENTS_DIR:f.attachmentsDir||"(memory disabled)",TIMEZONE:m.timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,CURRENT_DATE:(new Date).toLocaleDateString("en-US",{timeZone:m.timezone||void 0,weekday:"long",year:"numeric",month:"long",day:"numeric"}),SUBAGENT_TASK:O??"",NODE_TOOLS_INSTRUCTIONS:y?h():"",MESSAGE_TOOLS_INSTRUCTIONS:I?u():"",HEARTBEAT_INSTRUCTIONS:m.cron.enabled?g(m.cron.heartbeat.message):"",HEARTBEAT_PROMPT:m.cron.enabled?m.cron.heartbeat.message:"",CLAUDE_BUILT_IN_TOOLS:_?"":r(m.dataDir,E),SEARCH_IN_MEMORIES:"builtin-only"!==m.memory.recallStrategy?p():"",AVAILABLE_TOOLS:S(A),MESH_INFO:m.channels.mesh.enabled?(b=m.channels.mesh.agentId,["## Mesh (Inter-Agent Communication)","",`You are connected to the Hera Mesh as **${b}**.`,"The mesh allows direct communication between agents (e.g. Dante ↔ Beatrice).","","- To see who's online: use the **mesh-agents** skill",'- To send a message: `send_message(channel="mesh", chatId="<agentId>", text="...")`',"- Incoming mesh messages appear as normal messages from channel=mesh",""].join("\n")):"",RUNTIME_LINE:T(m,f),WORKSPACE_FILES:i(N,m.dataDir)}).replace(/\n{3,}/g,"\n\n");return d.debug(`System prompt built (mode=${E}, template=${w}, length=${M.length})`),M}export function buildSystemPromptWithMeta(i){const{config:m,sessionContext:d,mode:f,hasNodeTools:E,hasMessageTools:y,coderSkill:I,subagentTask:_,toolServers:O}=i,A="minimal"===f?"SYSTEM_PROMPT_SUBAGENT.md":"SYSTEM_PROMPT.md",w=a(m.dataDir,A),R="minimal"===f?c(i.workspaceFiles):i.workspaceFiles,{formatted:N,meta:b}=l(R,m.dataDir);return{prompt:resolvePlaceholders(w,{SESSION_KEY:d.sessionKey,CHANNEL:d.channel,CHAT_ID:d.chatId,SESSION_ID:d.sessionId||"(new session)",MODEL:s(m.agent.model),HOSTNAME:e(),OS:`${n()} ${t()} (${o()})`,WORKSPACE_DIR:m.agent.workspacePath,DATA_DIR:m.dataDir,MEMORY_FILE:d.memoryFile||"(memory disabled)",ATTACHMENTS_DIR:d.attachmentsDir||"(memory disabled)",TIMEZONE:m.timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,CURRENT_DATE:(new Date).toLocaleDateString("en-US",{timeZone:m.timezone||void 0,weekday:"long",year:"numeric",month:"long",day:"numeric"}),SUBAGENT_TASK:_??"",NODE_TOOLS_INSTRUCTIONS:E?h():"",MESSAGE_TOOLS_INSTRUCTIONS:y?u():"",HEARTBEAT_INSTRUCTIONS:m.cron.enabled?g(m.cron.heartbeat.message):"",HEARTBEAT_PROMPT:m.cron.enabled?m.cron.heartbeat.message:"",CLAUDE_BUILT_IN_TOOLS:I?"":r(m.dataDir,f),SEARCH_IN_MEMORIES:"builtin-only"!==m.memory.recallStrategy?p():"",AVAILABLE_TOOLS:S(O),RUNTIME_LINE:T(m,d),WORKSPACE_FILES:N}).replace(/\n{3,}/g,"\n\n"),meta:b}}export function resolvePlaceholders(e,n){return e.replace(/\{\{(\w+)\}\}/g,(e,t)=>t in n?n[t]:(d.warn(`Unknown placeholder: {{${t}}}`),`{{${t}}}`))}function h(){return"# Remote Nodes\n\nYou have access to remote nodes — external machines that you can control. Use the node tools to discover and interact with them.\n\n## Available tools\n\n- **list_nodes**: Call this to see which nodes are currently connected. Returns each node's ID, name, platform, hostname, and available commands. Always call this first before trying to execute commands, so you know which nodes are online and what their IDs are.\n\n- **node_exec**: Execute a command on a specific node. You must provide the nodeId (from list_nodes), the command name, and its parameters.\n\n## Supported commands\n\n- **shell.run**: Run a shell command on the node. Params: { cmd: string, args?: string[], cwd?: string, timeout?: number, env?: Record<string,string> }. Returns { stdout, stderr, exitCode }.\n- **shell.which**: Check if a binary exists on the node. Params: { cmd: string }. Returns { path } or null.\n\n## Guidelines\n\n- Always call list_nodes first to discover available nodes and their IDs. Do not guess node IDs.\n- When a user asks to run something on a remote machine, a node, or a specific hostname, call list_nodes to see what's online, then use node_exec with the appropriate nodeId.\n- If multiple nodes are connected, ask the user which one to use when the intent is ambiguous.\n- If no nodes are connected, inform the user that no remote nodes are available.\n- Report command results clearly: show stdout, note any stderr, and mention non-zero exit codes."}function u(){return"# Messaging\n\nYou have tools to send messages to chat channels. Each message you process includes a <session_info> block with the current channel and chatId.\n\n## Available tools\n\n- **send_message**: Send a text message to a specific channel and chat. Use the channel and chatId from the session context to reply on the current conversation. You can also send to a different channel or chatId if instructed.\n\n- **list_channels**: List all registered channels. Returns each channel's name and whether it is active.\n\n## Guidelines\n\n- Use send_message when you need to proactively send a message outside of the normal response flow (e.g. notifications, forwarding, or sending to a different chat).\n- Your normal response text is already delivered to the user. Only use send_message for additional messages or cross-channel communication.\n- The channel and chatId from the session context identify the current conversation. Use them to send follow-up messages to the same chat.\n- If the user asks you to message someone on a different channel or chat, use the appropriate channel name and chatId.\n- Never spam or send unsolicited messages. Only send when explicitly asked or when it is clearly part of the task."}function g(e){return`# Heartbeats\n\nHeartbeat prompt: ${e}\nIf you receive a heartbeat poll (a user message matching the heartbeat prompt above), and there is nothing that needs attention, reply exactly:\nHEARTBEAT_OK\nThe system treats a leading/trailing "HEARTBEAT_OK" as a heartbeat ack and may suppress it (not deliver to the user).\nIf something needs attention, do NOT include "HEARTBEAT_OK"; reply with the alert text instead.`}function p(){return"## Memory Search Tools\n\nYou have access to memory search tools for recalling past conversations and knowledge:\n\n- `memory_search` — semantically searches Markdown chunks (~400 token target, 80-token overlap) from `MEMORY.md` + `memory/**/*.md`. It returns snippet text (capped ~700 chars), file path, line range, and score. No full file payload is returned.\n- `memory_get` — reads a specific memory Markdown file (workspace-relative), optionally from a starting line and for N lines. Paths outside `MEMORY.md` / `memory/` are rejected.\n\nUse `memory_search` to find relevant past context before answering questions that might relate to previous conversations. Use `memory_get` to read the full content of a memory file when a search snippet is not enough."}function S(e){if(!e||0===e.length)return"";const n=[];for(const t of e){const e=t,o=e.instance?._registeredTools;if(o)for(const[e,t]of Object.entries(o)){const o=t.description||"",s=t.inputSchema?.def?.shape,a=[];if(s)for(const[e,n]of Object.entries(s)){let t=n.type||"unknown";"ZodNumber"===t?t="number":"ZodString"===t?t="string":"ZodBoolean"===t?t="boolean":"ZodArray"===t?t="array":"ZodObject"===t?t="object":"ZodEnum"===t&&(t="enum");const o=n.isOptional?.()??!1;a.push(`${e}${o?"?":""}: ${t}`)}const r=a.length>0?`Params: { ${a.join(", ")} }`:"No parameters.",i=o.match(/\.\s*(Returns?\s.+?)\.?\s*$/i),l=i?i[1].trim():"";let c=`- **${e}**: ${i?o.slice(0,i.index).trim():o.trim()}. ${r}`;l&&(c+=`. ${l}.`),n.push(c)}}return 0===n.length?"":n.join("\n")}function T(a,r){return`host=${e()} | os=${n()} ${t()} (${o()}) | model=${s(a.agent.model)} | channel=${r.channel} | session=${r.sessionKey}`}export function buildPrompt(e,n,t,o,s){const a=[],r=[];if(o){const n=e.contentBlocks.find(e=>"text"===e.type)?.text??"",t=/^\[.*\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(n),s=/Current time: /.test(n),r=n.startsWith("/");if(!t&&!s&&!r){const e=new Date,n=new Intl.DateTimeFormat("en-US",{timeZone:o,weekday:"short"}).format(e),t=e.toLocaleString("en-US",{timeZone:o,year:"numeric"}),s=e.toLocaleString("en-US",{timeZone:o,month:"2-digit"}),r=e.toLocaleString("en-US",{timeZone:o,day:"2-digit"}),i=e.toLocaleString("en-GB",{timeZone:o,hour:"2-digit",minute:"2-digit",hour12:!1});a.push(`[${n} ${t}-${s}-${r} ${i}]`)}}if(n&&a.push("<conversation_history>",n,"</conversation_history>",""),s){const n=e.contentBlocks.find(e=>"text"===e.type)?.text??"",t=n.startsWith("/"),o=/Current time: /.test(n)||n.includes("[HEARTBEAT]");t||o||a.push(s)}for(const n of e.contentBlocks)"text"===n.type&&n.text?a.push(n.text):"image"===n.type&&n.imageBase64&&r.push({base64:n.imageBase64,mimeType:n.imageMimeType??"image/jpeg"});const i=e.savedFiles.filter(e=>!e.endsWith(".tgs"));if(i.length>0){a.push(""),a.push("Files available in the current working directory:");for(const e of i)a.push(`- ${e}`)}return{text:a.join("\n"),images:r}}
|
|
@@ -108,7 +108,7 @@ export declare class SessionAgent {
|
|
|
108
108
|
private messageSentViaTool;
|
|
109
109
|
private pendingPermission;
|
|
110
110
|
private pendingQuestion;
|
|
111
|
-
constructor(sessionKey: string, config: AppConfig, systemPrompt: string, subagentSystemPrompt: string, sessionId?: string, modelOverride?: string, nodeToolsServer?: unknown, messageToolsServer?: unknown, serverToolsServer?: unknown, cronToolsServer?: unknown, coderSkill?: boolean, subagentsEnabled?: boolean, customSubAgentsEnabled?: boolean, ttsToolsServer?: unknown, memoryToolsServer?: unknown, browserToolsServer?: unknown, sandboxEnabled?: boolean, picoToolsServer?: unknown, telegramToolsServer?: unknown, a2uiToolsServer?: unknown, dynamicUIToolsServer?: unknown, plasmaClientToolsServer?: unknown, conceptToolsServer?: unknown, stateToolsServer?: unknown);
|
|
111
|
+
constructor(sessionKey: string, config: AppConfig, systemPrompt: string, subagentSystemPrompt: string, sessionId?: string, modelOverride?: string, nodeToolsServer?: unknown, messageToolsServer?: unknown, serverToolsServer?: unknown, cronToolsServer?: unknown, coderSkill?: boolean, subagentsEnabled?: boolean, customSubAgentsEnabled?: boolean, ttsToolsServer?: unknown, memoryToolsServer?: unknown, browserToolsServer?: unknown, sandboxEnabled?: boolean, picoToolsServer?: unknown, telegramToolsServer?: unknown, a2uiToolsServer?: unknown, dynamicUIToolsServer?: unknown, plasmaClientToolsServer?: unknown, conceptToolsServer?: unknown, stateToolsServer?: unknown, operationalContextToolsServer?: unknown);
|
|
112
112
|
/**
|
|
113
113
|
* Resolve Pi provider configuration based on the active model.
|
|
114
114
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{query as e}from"@anthropic-ai/claude-agent-sdk";import{MessageQueue as s}from"./message-queue.js";import{resolveModelId as t,resolveModelEntry as i}from"../config.js";import{createLogger as o}from"../utils/logger.js";import{ToolLoopDetector as n}from"./tool-loop-detector.js";const r=o("SessionAgent");export class SessionAgent{sessionKey;config;queue;queryHandle=null;pendingResponses=[];currentResponse="";currentSessionId;model;primaryModel;fallbackActive=!1;queueMode;closed=!1;piProviderConfig=null;outputDone=!1;initialized=!1;opts;collectBuffer=[];lastCollectAt=0;debounceMs;debounceTimer=null;debounceResolve=null;queueCap;dropPolicy;droppedResolvers=[];droppedSummaries=[];sdkSlashCommands=[];channelSender=null;toolUseNotifier=null;typingSetter=null;typingClearer=null;textBlockStreamer=null;pendingTextBlock="";streamedAny=!1;streamedText="";usageRecorder=null;autoApproveTools;loopDetector;skillNudgeEnabled;skillNudgeThreshold;qualityGateEnabled;toolCallsCurrentTurn=0;toolCallsLastTurn=0;messageSentViaTool=!1;pendingPermission=null;pendingQuestion=null;constructor(e,i,o,l,a,h,u,d,c,p,g,f,m,y,$,b,v,T,w,S,k,R,K,x){this.sessionKey=e,this.config=i,this.currentSessionId=a??"";const _=h??i.agent.model;this.model=_?t(i,_):"",this.primaryModel=this.model,this.queueMode=i.agent.queueMode,this.debounceMs=Math.max(0,i.agent.queueDebounceMs),this.queueCap=Math.max(0,i.agent.queueCap),this.dropPolicy=i.agent.queueDropPolicy,this.autoApproveTools=i.agent.autoApproveTools,this.loopDetector=new n(e),this.skillNudgeEnabled=i.agent.skillNudge?.enabled??!0,this.skillNudgeThreshold=i.agent.skillNudge?.threshold??10,this.qualityGateEnabled=i.agent.qualityGate?.enabled??!1,this.queue=new s,this.opts={...this.model?{model:this.model}:{},systemPrompt:g?{type:"preset",preset:"claude_code",append:o}:o,...i.agent.maxTurns>0?{maxTurns:i.agent.maxTurns}:{},cwd:i.agent.workspacePath,env:process.env,permissionMode:i.agent.permissionMode,allowDangerouslySkipPermissions:!1,...v?{sandbox:{enabled:!0,autoAllowBashIfSandboxed:!0,network:{allowLocalBinding:!0}}}:{},canUseTool:async(e,s)=>this.handleCanUseTool(e,s),hooks:{PreCompact:[{hooks:[async e=>{const s=e?.trigger??"auto";if(r.info(`[${this.sessionKey}] PreCompact hook fired (trigger=${s})`),this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if("cron"!==t){const e="auto"===s?"The conversation context is getting large — compacting memory to keep things running smoothly.":"Compacting conversation memory...";this.channelSender(t,i,e).catch(()=>{})}}}return{}}]}]},stderr:s=>{r.error(`[${e}] SDK stderr: ${s.trimEnd()}`)}};const P=i.agent.settingSources;"user"===P?this.opts.settingSources=["user"]:"project"===P?this.opts.settingSources=["project"]:"both"===P&&(this.opts.settingSources=["user","project"]);const C=i.agent.mainFallback;C&&(this.opts.fallbackModel=t(i,C)),i.agent.allowedTools.length>0&&(this.opts.allowedTools=i.agent.allowedTools),i.agent.disallowedTools.length>0&&(this.opts.disallowedTools=i.agent.disallowedTools);const A={};if(Object.keys(i.agent.mcpServers).length>0&&Object.assign(A,i.agent.mcpServers),u&&(A["node-tools"]=u),d&&(A["message-tools"]=d),c&&(A["server-tools"]=c),p&&(A["cron-tools"]=p),y&&(A["tts-tools"]=y),$&&(A["memory-tools"]=$),b&&(A["browser-tools"]=b),T&&(A["pico-tools"]=T),w&&(A["telegram-actions"]=w),S&&(A["a2ui-tools"]=S),k&&(A["dynamic-ui-tools"]=k),R&&(A["plasma-client-tools"]=R),K&&(A["concept-tools"]=K),x&&(A["state-tools"]=x),Object.keys(A).length>0&&(this.opts.mcpServers=A,this.opts.allowedTools&&this.opts.allowedTools.length>0))for(const e of Object.keys(A)){const s=`mcp__${e}__*`;this.opts.allowedTools.includes(s)||this.opts.allowedTools.push(s)}if(a&&(this.opts.resume=a),!1===f&&(this.opts.allowedTools&&this.opts.allowedTools.length>0?this.opts.allowedTools=this.opts.allowedTools.filter(e=>"Task"!==e):(this.opts.disallowedTools||(this.opts.disallowedTools=[]),this.opts.disallowedTools.includes("Task")||this.opts.disallowedTools.push("Task"))),m){const e={};for(const s of i.agent.customSubAgents){if(!s.enabled)continue;const t=s.expandContext?l+"\n\n"+s.prompt:s.prompt;e[s.name]={description:s.description,prompt:t,tools:s.tools,..."inherit"!==s.model?{model:s.model}:{}}}Object.keys(e).length>0&&(this.opts.agents=e)}const I=i.agent.plugins.filter(e=>e.enabled);I.length>0&&(this.opts.options={...this.opts.options,plugins:I.map(e=>({type:"local",path:e.path}))});const q=this.buildEnvForModel(this.model);this.opts.env=q.env,q.disableThinking&&(this.opts.maxThinkingTokens=0),this.piProviderConfig=this.resolvePiConfig(),this.piProviderConfig&&r.info(`[${e}] Pi engine: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}resolvePiConfig(){const e=this.model,s=this.config.models?.find(s=>s.id===e),t=s?.name??"";let i;const o=this.config.agent.picoAgent;if(o?.enabled&&Array.isArray(o.modelRefs)&&(i=o.modelRefs.find(e=>e.split(":")[0]===t)),!i){const e=this.config.agent.engine;if(!e||"pi"!==e.type||!e.piModelRef)return null;i=e.piModelRef}const n=i.split(":");if(n.length<2)return r.warn(`[${this.currentSessionId}] Invalid piModelRef (missing ':'): ${i}`),null;const l=n[0].trim();let a,h;if(n.length>=3)a=n[1].trim(),h=n.slice(2).join(":").trim(),h.startsWith(a+":")&&(h=h.substring(a.length+1));else{const e=n[1].trim(),s=e.indexOf("/");s>0?(a=e.substring(0,s),h=e.substring(s+1)):(a="openrouter",h=e)}const u=this.config.models?.find(e=>e.name===l);let d,c;u?.baseURL&&u.baseURL.includes("openrouter.ai")&&"openrouter"!==a&&(r.info(`[${this.currentSessionId}] piModelRef auto-correction: baseURL is openrouter.ai, switching provider from "${a}" to "openrouter" (modelId: "${a}/${h}")`),h=`${a}/${h}`,a="openrouter"),r.info(`[${this.currentSessionId}] piModelRef resolved: provider="${a}", modelId="${h}", contextWindow=${u?.contextWindow??128e3}`);const p=o?.rollingMemoryModel;if(p){const e=p.split(":");if(e.length>=3)d=e[1].trim(),c=e.slice(2).join(":").trim();else if(2===e.length){const s=e[1].indexOf("/");s>0?(d=e[1].substring(0,s).trim(),c=e[1].substring(s+1).trim()):c=e[1].trim()}c&&r.info(`[${this.currentSessionId}] Summarization model resolved: ${d}/${c}`)}return{provider:a,modelId:h,apiKey:u?.apiKey||void 0,baseUrl:u?.baseURL||void 0,contextWindowTokens:u?.contextWindow||void 0,costInput:u?.costInput||void 0,costOutput:u?.costOutput||void 0,costCacheRead:u?.costCacheRead||void 0,costCacheWrite:u?.costCacheWrite||void 0,summarizationProvider:d,summarizationModelId:c}}static API_RETRY_RE=/API Error:\s*(?:500|502|503|529)|overloaded|internal server error/i;async send(e,s){const t=s??0;if(this.closed||this.outputDone)throw new Error("SessionAgent is closed");if(this.skillNudgeEnabled&&this.toolCallsLastTurn>=this.skillNudgeThreshold&&0===t){const s=`[System note: Your previous turn used ${this.toolCallsLastTurn} tool calls. If you discovered a reusable workflow or improved on an existing skill's approach, consider updating or creating a skill (Edit the SKILL.md file directly). Skip this if the task was inherently complex and doesn't generalize.]`;e={...e,text:s+"\n\n"+e.text},this.toolCallsLastTurn=0,r.info(`[${this.sessionKey}] Skill nudge injected (previous turn had ${this.skillNudgeThreshold}+ tool calls)`)}let o;switch(this.loopDetector.reset(),this.ensureInitialized(),this.queueMode){case"collect":o=await this.sendCollect(e);break;case"steer":o=await this.sendSteer(e);break;default:o=await this.sendDirect(e)}const n=o.fullResponse??o.response,l=SessionAgent.API_RETRY_RE.test(n),a=this.config.agent.apiRetry,h=a.maxAttempts;if(l&&t<h){const s=t+1,i=Math.min(a.baseDelayMs*2**t,a.maxDelayMs);if(r.warn(`[${this.sessionKey}] Transient API error detected, retry ${s}/${h} in ${i}ms`),this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if("cron"!==t){const e=`API temporarily unavailable, retrying (attempt ${s}/${h})...`;this.channelSender(t,i,e).catch(()=>{})}}}return await new Promise(e=>setTimeout(e,i)),this.send(e,s)}if(l&&t>=h){r.error(`[${this.sessionKey}] API error persists after ${h} retries`);const s=this.opts.fallbackModel;if(s&&!this.fallbackActive&&this.model!==s){const t=i(this.config,s);if(t?.types?.includes("internal")??!1){if(r.warn(`[${this.sessionKey}] Switching to fallback model: ${s}`),await this.setModel(s),this.fallbackActive=!0,this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const i=this.sessionKey.substring(0,e),o=this.sessionKey.substring(e+1);if("cron"!==i){const e=`⚠️ Primary model unavailable after ${h} retries. Switching to fallback (${t?.name??s}). Use /model or /new to restore.`;this.channelSender(i,o,e).catch(()=>{})}}}return this.send(e,0)}}if(this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if("cron"!==s){const e=`API did not respond after ${h} attempts. Please try again later.`;this.channelSender(s,t,e).catch(()=>{})}}}}const u=!0===e.__qualityGateRetry;if(!this.piProviderConfig&&this.qualityGateEnabled&&!u){const e=o.fullResponse??o.response;if(e&&e.length>50)try{const{runQualityGate:s}=await import("./quality-gate.js"),t=await s(e);if(!t.pass){r.warn(`[${this.sessionKey}] QualityGate FAILED (${t.durationMs}ms): ${JSON.stringify(t.flags)}`);const e={text:`[Quality Gate FAILED] Your previous response contains unverified claims:\n${t.flags.map(e=>`- ${e}`).join("\n")}\n\nReconsider your response. Verify the flagged claims (use tools if needed) and respond again with a corrected version. Do NOT mention the quality gate to the user.`,images:[],__qualityGateRetry:!0};r.info(`[${this.sessionKey}] QualityGate: re-prompting agent for self-correction`),o=await this.send(e,0)}}catch(e){r.warn(`[${this.sessionKey}] QualityGate error (fail-open): ${e}`)}}return o}async interrupt(){if(this.closed||!this.queryHandle)return!1;try{return await this.queryHandle.interrupt(),r.info(`[${this.sessionKey}] Interrupted`),!0}catch{return!1}}async setModel(e){if(this.queryHandle)try{await this.queryHandle.setModel(e),this.model=e,this.fallbackActive=!1,r.info(`[${this.sessionKey}] Model changed to ${e}`)}catch(e){r.error(`[${this.sessionKey}] Failed to set model: ${e}`)}}close(){if(this.closed)return;if(this.closed=!0,this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.debounceResolve&&(this.debounceResolve(),this.debounceResolve=null),this.queue.close(),this.queryHandle){const e=this.queryHandle;this.queryHandle=null,queueMicrotask(()=>{try{e.close()}catch{}})}const e=new Error("SessionAgent closed");for(const s of this.pendingResponses)s.reject(e);for(const s of this.collectBuffer)s.reject(e);for(const s of this.droppedResolvers)s.reject(e);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[],r.info(`[${this.sessionKey}] Closed`)}isActive(){return!this.closed&&!this.outputDone}getSessionId(){return this.currentSessionId}getModel(){return this.model}isFallbackActive(){return this.fallbackActive}getSdkSlashCommands(){return this.sdkSlashCommands}setChannelSender(e){this.channelSender=e}setToolUseNotifier(e){this.toolUseNotifier=e}setTypingSetter(e){this.typingSetter=e}setTypingClearer(e){this.typingClearer=e}setTextBlockStreamer(e){this.textBlockStreamer=e}setUsageRecorder(e){this.usageRecorder=e}buildEnvForModel(e){const s=this.config.models.find(s=>s.id===e);if(!s?.proxy||"not-used"===s.proxy)return{env:{...process.env},proxied:!1,disableThinking:!1};const t={...process.env};return"direct"===s.proxy?(t.ANTHROPIC_BASE_URL=s.baseURL,t.ANTHROPIC_AUTH_TOKEN=s.apiKey,t.ANTHROPIC_API_KEY="",r.info(`[${this.sessionKey}] Direct env applied for model ${e} (url=${t.ANTHROPIC_BASE_URL})`),{env:t,proxied:!0,disableThinking:!1}):(t.ANTHROPIC_BASE_URL=s.fastUrl||this.config.fastProxyUrl,t.ANTHROPIC_AUTH_TOKEN=s.fastProxyApiKey,t.ANTHROPIC_API_KEY="",delete t.ANTHROPIC_BETAS,delete t.CLAUDE_CODE_EXTRA_BODY,r.info(`[${this.sessionKey}] Proxy env applied for model ${e} (url=${t.ANTHROPIC_BASE_URL})`),{env:t,proxied:!0,disableThinking:!0})}hasPendingPermission(){return null!==this.pendingPermission}resolvePermission(e){if(!this.pendingPermission)return;const s=this.pendingPermission;this.pendingPermission=null,e?(r.info(`[${this.sessionKey}] Permission approved: ${s.toolName}`),s.resolve({behavior:"allow",updatedInput:s.input})):(r.info(`[${this.sessionKey}] Permission denied: ${s.toolName}`),s.resolve({behavior:"deny",message:"User denied this action"}))}isBusy(){return this.pendingResponses.length>0}hasPendingQuestion(){return null!==this.pendingQuestion}resolveQuestion(e){if(!this.pendingQuestion)return;const s=this.pendingQuestion;this.pendingQuestion=null,r.info(`[${this.sessionKey}] Question answered: "${e}" for "${s.questionText}"`),s.resolve(e)}async handleCanUseTool(e,s){if("AskUserQuestion"===e){if(!this.channelSender)return r.warn(`[${this.sessionKey}] No channel sender for AskUserQuestion, auto-approving`),{behavior:"allow",updatedInput:s};const e=this.sessionKey.indexOf(":");if(e<0)return{behavior:"allow",updatedInput:s};const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if(!t||!i||"cron"===t)return{behavior:"allow",updatedInput:s};const o=s?.questions;if(!Array.isArray(o)||0===o.length)return{behavior:"allow",updatedInput:s};const n={};for(const e of o){const o=e.question||"?",l=Array.isArray(e.options)?e.options:[],a=[];if(e.header&&a.push(`*${e.header}*`),a.push(o),l.some(e=>e.description)){a.push("");for(const e of l){const s=e.description?`: ${e.description}`:"";a.push(`• ${e.label}${s}`)}}const h=a.join("\n");if(this.typingClearer)try{await this.typingClearer(t,i)}catch{}try{if(l.length>0){const e=l.map(e=>({text:e.label||String(e),callbackData:`__ask:${e.label||String(e)}`}));await this.channelSender(t,i,h,e)}else await this.channelSender(t,i,h)}catch(e){return r.error(`[${this.sessionKey}] Failed to send AskUserQuestion: ${e}`),{behavior:"allow",updatedInput:s}}const u=55e3,d=await new Promise(e=>{const s=setTimeout(()=>{if(this.pendingQuestion){this.pendingQuestion=null;const s=l.length>0?l[0].label||String(l[0]):"No answer";r.warn(`[${this.sessionKey}] Question timeout, defaulting to "${s}"`),this.channelSender&&this.channelSender(t,i,`[Timeout] Auto-selected: ${s}`).catch(()=>{}),e(s)}},u);this.pendingQuestion={resolve:t=>{clearTimeout(s),e(t)},questionText:o}});if(n[o]=d,this.typingSetter)try{await this.typingSetter(t,i)}catch{}}return r.info(`[${this.sessionKey}] AskUserQuestion answered: ${JSON.stringify(n)}`),{behavior:"allow",updatedInput:{questions:s.questions,answers:n}}}const t=this.loopDetector.check(e,s);if("circuit_break"===t.severity)return r.error(`[${this.sessionKey}] CIRCUIT BREAK: ${t.reason}`),{behavior:"deny",message:`[Loop detected] ${t.reason} Try a different approach or ask the user for help.`};if(this.autoApproveTools)return r.debug(`[${this.sessionKey}] Auto-approving tool: ${e}`),{behavior:"allow",updatedInput:s};if(!this.channelSender)return r.warn(`[${this.sessionKey}] No channel sender for interactive permission, auto-approving: ${e}`),{behavior:"allow",updatedInput:s};const i=this.sessionKey.indexOf(":");if(i<0)return{behavior:"allow",updatedInput:s};const o=this.sessionKey.substring(0,i),n=this.sessionKey.substring(i+1);if(!o||!n||"cron"===o)return{behavior:"allow",updatedInput:s};const l=[`[Permission Request] Tool: ${e}`];if("Bash"===e&&s?.command)l.push(`Command: ${s.command}`),s.description&&l.push(`Description: ${s.description}`);else if("Write"===e&&s?.file_path)l.push(`File: ${s.file_path}`);else if("Edit"===e&&s?.file_path)l.push(`File: ${s.file_path}`);else if("ExitPlanMode"===e&&s?.plan){if(l.push(""),l.push(s.plan),Array.isArray(s.allowedPrompts)&&s.allowedPrompts.length>0){l.push(""),l.push("Requested permissions:");for(const e of s.allowedPrompts)l.push(` - [${e.tool}] ${e.prompt}`)}}else{const e=JSON.stringify(s);e.length<=300?l.push(`Input: ${e}`):l.push(`Input: ${e.slice(0,297)}...`)}l.push(""),l.push("Reply: approve to allow, deny to reject");const a=l.join("\n"),h=[{text:"Approve",callbackData:"__tool_perm:approve"},{text:"Deny",callbackData:"__tool_perm:deny"}];try{await this.channelSender(o,n,a,h)}catch(e){return r.error(`[${this.sessionKey}] Failed to send permission request: ${e}`),{behavior:"allow",updatedInput:s}}if(this.typingClearer)try{await this.typingClearer(o,n)}catch{}const u=12e4;return new Promise(t=>{const i=setTimeout(()=>{this.pendingPermission?.resolve===t&&(this.pendingPermission=null,r.warn(`[${this.sessionKey}] Permission timeout for ${e}, auto-denying`),this.channelSender&&this.channelSender(o,n,`[Permission timeout] Tool ${e} denied after 120s`).catch(()=>{}),t({behavior:"deny",message:"Permission request timed out"}))},u);this.pendingPermission={resolve:t,toolName:e,input:s};const l=t;this.pendingPermission.resolve=e=>{clearTimeout(i),l(e)}})}async forwardAskUserQuestion(e){if(!this.channelSender)return;const s=this.sessionKey.indexOf(":");if(s<0)return;const t=this.sessionKey.substring(0,s),i=this.sessionKey.substring(s+1);if(!t||!i||"cron"===t)return;const o=e?.questions;if(Array.isArray(o)){for(const e of o){const s=e.question||"?",o=Array.isArray(e.options)?e.options:[],n=[];if(e.header&&n.push(`*${e.header}*`),n.push(s),o.some(e=>e.description)){n.push("");for(const e of o){const s=e.description?`: ${e.description}`:"";n.push(`• ${e.label}${s}`)}}const l=n.join("\n");try{if(o.length>0){const e=o.map(e=>({text:e.label||String(e),callbackData:e.label||String(e)}));await this.channelSender(t,i,l,e)}else await this.channelSender(t,i,l)}catch(e){r.error(`[${this.sessionKey}] Failed to forward AskUserQuestion: ${e}`)}}if(this.typingClearer)try{await this.typingClearer(t,i)}catch(e){r.error(`[${this.sessionKey}] Failed to clear typing: ${e}`)}}}async notifyToolUse(e){if(!this.toolUseNotifier)return;const s=this.sessionKey.indexOf(":");if(s<0)return;const t=this.sessionKey.substring(0,s),i=this.sessionKey.substring(s+1);if(t&&i&&"cron"!==t)try{await this.toolUseNotifier(t,i,e)}catch(e){r.error(`[${this.sessionKey}] Failed to notify tool use: ${e}`)}}async flushPendingTextBlock(){if(!this.textBlockStreamer||!this.pendingTextBlock)return;const e=this.sessionKey.indexOf(":");if(e<0)return;const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if(!s||!t||"cron"===s)return;const i=this.pendingTextBlock;this.pendingTextBlock="",this.streamedAny=!0,this.streamedText+=i;try{await this.textBlockStreamer(s,t,i)}catch(e){r.error(`[${this.sessionKey}] Text block stream error: ${e}`)}}sendDirect(e){if(this.queueCap>0&&this.pendingResponses.length>=this.queueCap)return r.warn(`[${this.sessionKey}] Queue cap reached (${this.queueCap}), rejecting message`),Promise.resolve({response:"Queue is full. Please wait for the current processing to complete.",sessionId:this.currentSessionId,sessionReset:!1});const s=this.buildQueueMessage(e);return new Promise((e,t)=>{this.pendingResponses.push({resolve:e,reject:t}),this.queue.push(s),r.info(`[${this.sessionKey}] Message queued (pending=${this.pendingResponses.length})`)})}sendCollect(e){return this.pendingResponses.length>0?this.queueCap>0&&this.collectBuffer.length>=this.queueCap?this.applyDropPolicy(e):(this.lastCollectAt=Date.now(),r.info(`[${this.sessionKey}] Collecting message (buffer=${this.collectBuffer.length+1})`),new Promise((s,t)=>{this.collectBuffer.push({prompt:e,resolve:s,reject:t})})):this.sendDirect(e)}async sendSteer(e){return this.pendingResponses.length>0&&(r.info(`[${this.sessionKey}] Steer: interrupting current processing`),await this.interrupt()),this.sendDirect(e)}applyDropPolicy(e){if("new"===this.dropPolicy)return r.warn(`[${this.sessionKey}] Queue cap reached, rejecting new message`),Promise.resolve({response:"Queue is full. Please wait for the current processing to complete.",sessionId:this.currentSessionId,sessionReset:!1});const s=this.collectBuffer.shift();return"summarize"===this.dropPolicy&&this.droppedSummaries.push(function(e,s){const t=e.replace(/\s+/g," ").trim();return t.length<=s?t:`${t.slice(0,s-1).trimEnd()}…`}(s.prompt.text,140)),this.droppedResolvers.push({resolve:s.resolve,reject:s.reject}),r.warn(`[${this.sessionKey}] Queue cap reached, dropped oldest message (policy=${this.dropPolicy}, dropped=${this.droppedResolvers.length})`),this.lastCollectAt=Date.now(),new Promise((s,t)=>{this.collectBuffer.push({prompt:e,resolve:s,reject:t})})}async debounceThenFlush(){if(this.debounceMs<=0||this.closed)this.flushCollectBuffer();else{for(;!this.closed&&this.collectBuffer.length>0;){const e=Date.now()-this.lastCollectAt;if(e>=this.debounceMs)break;await new Promise(s=>{this.debounceResolve=s,this.debounceTimer=setTimeout(s,this.debounceMs-e)}),this.debounceTimer=null,this.debounceResolve=null}this.closed||this.flushCollectBuffer()}}flushCollectBuffer(){if(0===this.collectBuffer.length&&0===this.droppedResolvers.length)return;const e=this.collectBuffer.splice(0),s=e.map(e=>e.prompt),t=this.mergePrompts(s),i=this.buildQueueMessage(t),o=[...this.droppedResolvers.splice(0),...e.map(e=>({resolve:e.resolve,reject:e.reject}))];this.droppedSummaries=[],this.pendingResponses.push({resolve:e=>{for(const s of o)s.resolve(e)},reject:e=>{for(const s of o)s.reject(e)}}),this.queue.push(i),r.info(`[${this.sessionKey}] Flushed ${e.length} collected message(s) as one prompt`)}mergePrompts(e){const s=[],t=[];if(this.droppedSummaries.length>0){s.push(`[${this.droppedSummaries.length} earlier message(s) dropped due to queue cap]`);for(const e of this.droppedSummaries)s.push(`- ${e}`);s.push("")}if(1===e.length&&0===this.droppedSummaries.length)return e[0];if(e.length>0){s.push("[Queued messages while agent was busy]");for(let i=0;i<e.length;i++)e[i].text&&s.push(`${i+1}. ${e[i].text}`),t.push(...e[i].images)}return{text:s.join("\n"),images:t}}ensureInitialized(){if(this.initialized)return;this.initialized=!0;const s=this.piProviderConfig?"pi":"claudecode";r.info(`[${this.sessionKey}] Starting agent: engine=${s}, model=${this.model}, mode=${this.queueMode}, debounce=${this.debounceMs}ms, cap=${this.queueCap||"unlimited"}, drop=${this.dropPolicy}, session=${this.currentSessionId||"new"}`),this.piProviderConfig?this.initPiEngine():(this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput())}async initPiEngine(){try{const e=await import("../pi-agent-provider/index.js"),s=await e.createToolRegistryFromOptions(this.opts);this.queryHandle=e.piQuery({prompt:this.queue,options:this.opts},this.piProviderConfig,s),this.processOutput(),r.info(`[${this.sessionKey}] Pi engine initialized: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}catch(s){r.error(`[${this.sessionKey}] Failed to initialize Pi engine: ${s}`),r.warn(`[${this.sessionKey}] Falling back to Claude SDK (claudecode engine)`),this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput()}}buildQueueMessage(e){if(0===e.images.length)return r.debug(`[${this.sessionKey}] SDK request: text-only (${e.text.length} chars): ${this.config.verboseDebugLogs?e.text:e.text.slice(0,15)+"..."}`),{type:"user",message:{role:"user",content:e.text}};const s=[];for(const t of e.images)s.push({type:"image",source:{type:"base64",media_type:t.mimeType,data:t.base64}});return e.text&&s.push({type:"text",text:e.text}),r.debug(`[${this.sessionKey}] SDK request: ${s.length} block(s) [${s.map(e=>"image"===e.type?`image/${e.source.media_type}`:`text(${e.text?.length??0})`).join(", ")}]`),{type:"user",message:{role:"user",content:s}}}async processOutput(){if(this.queryHandle)try{for await(const e of this.queryHandle){if(this.closed)break;if(r.debug(`[${this.sessionKey}] SDK message: type=${e.type}, subtype=${e.subtype??"-"}, keys=${Object.keys(e).join(",")}`),"system"===e.type){const s=e,t=s.subtype;if("init"===t){const e=s.slash_commands;Array.isArray(e)&&(this.sdkSlashCommands=e.map(e=>e.replace(/^\//,"")))}if("compact_boundary"===t){const e=s.compact_metadata,t=["Context compacted."];e?.pre_tokens&&t.push(`Pre-compaction tokens: ${e.pre_tokens}.`),e?.trigger&&t.push(`Trigger: ${e.trigger}.`),this.currentResponse=t.join(" "),r.info(`[${this.sessionKey}] Compact: ${this.currentResponse}`)}else if("init"!==t&&"status"!==t){const e=new Set(["task_started","task_notification","files_persisted","hook_started","hook_progress","hook_response"]),{type:i,...o}=s,n=JSON.stringify(o,null,2);e.has(t)?r.debug(`[${this.sessionKey}] Internal SDK event (${t}): ${n.slice(0,200)}`):(this.currentResponse=n,r.info(`[${this.sessionKey}] System message (${t??"unknown"}): ${n.slice(0,200)}`))}}if("assistant"===e.type){const s=e.message.content,t=s.filter(e=>"text"===e.type).map(e=>e.text).join("");t&&(this.currentResponse=t,this.pendingTextBlock=t);const i=s.map(e=>e.type).join(", ");r.debug(`[${this.sessionKey}] SDK assistant message: blocks=[${i}], text length=${t.length}: ${this.config.verboseDebugLogs?t:t.slice(0,15)+"..."}`);s.some(e=>"tool_use"===e.type)&&this.pendingTextBlock&&this.textBlockStreamer&&!this.qualityGateEnabled&&await this.flushPendingTextBlock();for(const e of s)if("tool_use"===e.type){this.toolCallsCurrentTurn++,"mcp__message-tools__send_message"===e.name&&(this.messageSentViaTool=!0);const s=JSON.stringify(e.input);r.debug(`[${this.sessionKey}] Tool call: ${e.name} ${this.config.verboseDebugLogs?s:s.slice(0,100)+(s.length>100?"...":"")}`),this.toolUseNotifier&&"AskUserQuestion"!==e.name&&await this.notifyToolUse(e.name)}}if("tool_progress"===e.type){const s=e;r.debug(`[${this.sessionKey}] Tool progress: ${s.tool_name} (${s.elapsed_time_seconds}s)`)}if("result"===e.type){const s=e;let t;r.debug(`[${this.sessionKey}] SDK result: subtype=${s.subtype}, stop_reason=${s.stop_reason??"null"}, session=${s.session_id??"n/a"}, result length=${s.result?.length??0}`),"session_id"in s&&(this.currentSessionId=s.session_id),this.usageRecorder&&(void 0!==s.total_cost_usd||s.modelUsage)&&this.usageRecorder(this.sessionKey,s.total_cost_usd,s.duration_ms,s.num_turns,s.modelUsage);const i=s.stop_reason??null;if("success"===s.subtype){if(s.result)this.currentResponse=s.result;else if(!this.currentResponse&&!this.messageSentViaTool&&this.pendingResponses.length<=1&&(void 0!==s.total_cost_usd||s.usage)){const e=[];if(void 0!==s.total_cost_usd&&e.push(`Total cost: $${Number(s.total_cost_usd).toFixed(4)}`),void 0!==s.duration_ms&&e.push(`Duration: ${(s.duration_ms/1e3).toFixed(1)}s`),void 0!==s.num_turns&&e.push(`Turns: ${s.num_turns}`),s.modelUsage)for(const[t,i]of Object.entries(s.modelUsage)){const s=i,o=[` ${t}:`];s.inputTokens&&o.push(`input=${s.inputTokens}`),s.outputTokens&&o.push(`output=${s.outputTokens}`),s.cacheReadInputTokens&&o.push(`cache_read=${s.cacheReadInputTokens}`),s.cacheCreationInputTokens&&o.push(`cache_create=${s.cacheCreationInputTokens}`),void 0!==s.costUSD&&o.push(`cost=$${Number(s.costUSD).toFixed(4)}`),e.push(o.join(" "))}e.length>0&&(this.currentResponse=e.join("\n"))}if(!s.result&&!this.currentResponse&&this.pendingResponses.length<=1){const e=this.piProviderConfig;r.warn(`[${this.sessionKey}] Empty response on success: provider=${e?.provider??"sdk"}, modelId=${e?.modelId??"n/a"}, stop_reason=${i}. Check provider routing and API key.`)}"refusal"===i?(r.warn(`[${this.sessionKey}] Model refused the request`),this.currentResponse||(this.currentResponse="I'm unable to fulfill this request.")):"error"===i?(r.warn(`[${this.sessionKey}] Model returned stop_reason=error`),this.currentResponse||(this.currentResponse="⚠️ Request failed. The provider may be unavailable or the API key/credits may be exhausted.")):"max_tokens"===i&&r.warn(`[${this.sessionKey}] Response truncated: output token limit reached`)}else if("error_max_turns"===s.subtype)t="max_turns",r.warn(`[${this.sessionKey}] Max turns reached`);else if("error_max_budget_usd"===s.subtype)t="max_budget",r.warn(`[${this.sessionKey}] Max budget reached`);else{const e=s.errors??[];if(e.some(e=>e.includes("aborted")))r.info(`[${this.sessionKey}] Request aborted (steer interrupt)`);else if(r.error(`[${this.sessionKey}] SDK error: ${JSON.stringify(s)}`),!this.currentResponse&&!this.streamedAny){const s=e.filter(e=>!e.includes("aborted")).join("; ");this.currentResponse="⚠️ Request failed"+(s?`: ${s}`:". The provider may be unavailable or the API key/credits may be exhausted.")}}const o=this.pendingResponses.shift();if(o){const e=this.currentResponse||"";let s=e;this.streamedAny&&(s=this.pendingTextBlock||"",!s&&e&&e.length>this.streamedText.length&&(s=e.startsWith(this.streamedText)?e.slice(this.streamedText.length).replace(/^\n+/,""):e)),r.info(`[${this.sessionKey}] Response ready: session=${this.currentSessionId}, length=${s.length}${this.streamedAny?` (streamed, full=${e.length})`:""}`),o.resolve({response:s,fullResponse:this.streamedAny?e:void 0,sessionId:this.currentSessionId,sessionReset:!1,errorType:t,stopReason:i})}this.toolCallsLastTurn=this.toolCallsCurrentTurn,this.toolCallsCurrentTurn=0,this.messageSentViaTool=!1,this.currentResponse="",this.pendingTextBlock="",this.streamedAny=!1,this.streamedText="","collect"===this.queueMode&&(this.collectBuffer.length>0||this.droppedResolvers.length>0)&&await this.debounceThenFlush()}}}catch(e){r.error(`[${this.sessionKey}] Output stream error: ${e}`);const s=this.pendingResponses.shift();if(s)if(this.currentSessionId){const t=e instanceof Error?e.message:String(e);r.warn(`[${this.sessionKey}] Session error (${this.currentSessionId}): ${t}`),s.resolve({response:t,sessionId:"",sessionReset:!0})}else s.reject(e instanceof Error?e:new Error(String(e)));const t=new Error("SessionAgent terminated");for(const e of this.pendingResponses)e.reject(t);for(const e of this.collectBuffer)e.reject(t);for(const e of this.droppedResolvers)e.reject(t);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[]}finally{this.outputDone=!0}}}
|
|
1
|
+
import{query as e}from"@anthropic-ai/claude-agent-sdk";import{MessageQueue as s}from"./message-queue.js";import{resolveModelId as t,resolveModelEntry as i}from"../config.js";import{createLogger as o}from"../utils/logger.js";import{ToolLoopDetector as n}from"./tool-loop-detector.js";import{loadToolRules as r}from"../tools/operational-context-tools.js";const l=o("SessionAgent");export class SessionAgent{sessionKey;config;queue;queryHandle=null;pendingResponses=[];currentResponse="";currentSessionId;model;primaryModel;fallbackActive=!1;queueMode;closed=!1;piProviderConfig=null;outputDone=!1;initialized=!1;opts;collectBuffer=[];lastCollectAt=0;debounceMs;debounceTimer=null;debounceResolve=null;queueCap;dropPolicy;droppedResolvers=[];droppedSummaries=[];sdkSlashCommands=[];channelSender=null;toolUseNotifier=null;typingSetter=null;typingClearer=null;textBlockStreamer=null;pendingTextBlock="";streamedAny=!1;streamedText="";usageRecorder=null;autoApproveTools;loopDetector;skillNudgeEnabled;skillNudgeThreshold;qualityGateEnabled;toolCallsCurrentTurn=0;toolCallsLastTurn=0;messageSentViaTool=!1;pendingPermission=null;pendingQuestion=null;constructor(e,i,o,a,h,u,c,d,p,g,f,m,y,$,b,v,T,w,S,k,K,R,_,x,P){this.sessionKey=e,this.config=i,this.currentSessionId=h??"";const C=u??i.agent.model;this.model=C?t(i,C):"",this.primaryModel=this.model,this.queueMode=i.agent.queueMode,this.debounceMs=Math.max(0,i.agent.queueDebounceMs),this.queueCap=Math.max(0,i.agent.queueCap),this.dropPolicy=i.agent.queueDropPolicy,this.autoApproveTools=i.agent.autoApproveTools,this.loopDetector=new n(e),this.skillNudgeEnabled=i.agent.skillNudge?.enabled??!0,this.skillNudgeThreshold=i.agent.skillNudge?.threshold??10,this.qualityGateEnabled=i.agent.qualityGate?.enabled??!1,this.queue=new s,this.opts={...this.model?{model:this.model}:{},systemPrompt:f?{type:"preset",preset:"claude_code",append:o}:o,...i.agent.maxTurns>0?{maxTurns:i.agent.maxTurns}:{},cwd:i.agent.workspacePath,env:process.env,permissionMode:i.agent.permissionMode,allowDangerouslySkipPermissions:!1,...T?{sandbox:{enabled:!0,autoAllowBashIfSandboxed:!0,network:{allowLocalBinding:!0}}}:{},canUseTool:async(e,s)=>this.handleCanUseTool(e,s),hooks:{PreToolUse:[{hooks:[async e=>{try{const s=e?.tool_name;if(!s)return{};const t=r(i.dataDir);let o=t[s];if(!o){const e=s.includes("__")?s.split("__").pop():void 0;e&&(o=t[e])}return o&&0!==o.length?(l.info(`[${this.sessionKey}] CKR L1: injecting ${o.length} rules for tool ${s}`),{additionalContext:`[CKR Tool Rules for ${s}]\n${o.join("\n")}`}):{}}catch(e){return l.warn(`[${this.sessionKey}] CKR L1 error: ${e}`),{}}}]}],SubagentStart:[{hooks:[async e=>{const s=e?.agent_type??"unknown",t=e?.agent_id??"?";return l.info(`[${this.sessionKey}] Subagent START: ${s} (id=${t})`),this._subagentStartTimes??={},this._subagentStartTimes[t]=Date.now(),{}}]}],SubagentStop:[{hooks:[async e=>{const s=e?.agent_type??"unknown",t=e?.agent_id??"?",i=this._subagentStartTimes??{},o=i[t],n=o?Date.now()-o:-1,r=e?.last_assistant_message,a=r?r.substring(0,200):"(no output)";return l.info(`[${this.sessionKey}] Subagent STOP: ${s} (id=${t}) duration=${n}ms output=${a}`),o&&delete i[t],{}}]}],PreCompact:[{hooks:[async e=>{const s=e?.trigger??"auto";if(l.info(`[${this.sessionKey}] PreCompact hook fired (trigger=${s})`),this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if("cron"!==t){const e="auto"===s?"The conversation context is getting large — compacting memory to keep things running smoothly.":"Compacting conversation memory...";this.channelSender(t,i,e).catch(()=>{})}}}return{}}]}]},stderr:s=>{l.error(`[${e}] SDK stderr: ${s.trimEnd()}`)},...void 0!==i.agent.compaction?{compactionControl:i.agent.compaction}:{}};const A=i.agent.settingSources;"user"===A?this.opts.settingSources=["user"]:"project"===A?this.opts.settingSources=["project"]:"both"===A&&(this.opts.settingSources=["user","project"]);const I=i.agent.mainFallback;I&&(this.opts.fallbackModel=t(i,I)),i.agent.allowedTools.length>0&&(this.opts.allowedTools=i.agent.allowedTools),i.agent.disallowedTools.length>0&&(this.opts.disallowedTools=i.agent.disallowedTools);const q={};if(Object.keys(i.agent.mcpServers).length>0&&Object.assign(q,i.agent.mcpServers),c&&(q["node-tools"]=c),d&&(q["message-tools"]=d),p&&(q["server-tools"]=p),g&&(q["cron-tools"]=g),$&&(q["tts-tools"]=$),b&&(q["memory-tools"]=b),v&&(q["browser-tools"]=v),w&&(q["pico-tools"]=w),S&&(q["telegram-actions"]=S),k&&(q["a2ui-tools"]=k),K&&(q["dynamic-ui-tools"]=K),R&&(q["plasma-client-tools"]=R),_&&(q["concept-tools"]=_),x&&(q["state-tools"]=x),P&&(q["operational-context-tools"]=P),Object.keys(q).length>0&&(this.opts.mcpServers=q,this.opts.allowedTools&&this.opts.allowedTools.length>0))for(const e of Object.keys(q)){const s=`mcp__${e}__*`;this.opts.allowedTools.includes(s)||this.opts.allowedTools.push(s)}if(h&&(this.opts.resume=h),!1===m&&(this.opts.allowedTools&&this.opts.allowedTools.length>0?this.opts.allowedTools=this.opts.allowedTools.filter(e=>"Task"!==e):(this.opts.disallowedTools||(this.opts.disallowedTools=[]),this.opts.disallowedTools.includes("Task")||this.opts.disallowedTools.push("Task"))),y){const e={};for(const s of i.agent.customSubAgents){if(!s.enabled)continue;const t=s.expandContext?a+"\n\n"+s.prompt:s.prompt;e[s.name]={description:s.description,prompt:t,tools:s.tools,..."inherit"!==s.model?{model:s.model}:{}}}Object.keys(e).length>0&&(this.opts.agents=e)}const j=i.agent.plugins.filter(e=>e.enabled);j.length>0&&(this.opts.options={...this.opts.options,plugins:j.map(e=>({type:"local",path:e.path}))});const D=this.buildEnvForModel(this.model);this.opts.env=D.env,D.disableThinking&&(this.opts.maxThinkingTokens=0),this.piProviderConfig=this.resolvePiConfig(),this.piProviderConfig&&l.info(`[${e}] Pi engine: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}resolvePiConfig(){const e=this.model,s=this.config.models?.find(s=>s.id===e),t=s?.name??"";let i;const o=this.config.agent.picoAgent;if(o?.enabled&&Array.isArray(o.modelRefs)&&(i=o.modelRefs.find(e=>e.split(":")[0]===t)),!i){const e=this.config.agent.engine;if(!e||"pi"!==e.type||!e.piModelRef)return null;i=e.piModelRef}const n=i.split(":");if(n.length<2)return l.warn(`[${this.currentSessionId}] Invalid piModelRef (missing ':'): ${i}`),null;const r=n[0].trim();let a,h;if(n.length>=3)a=n[1].trim(),h=n.slice(2).join(":").trim(),h.startsWith(a+":")&&(h=h.substring(a.length+1));else{const e=n[1].trim(),s=e.indexOf("/");s>0?(a=e.substring(0,s),h=e.substring(s+1)):(a="openrouter",h=e)}const u=this.config.models?.find(e=>e.name===r);let c,d;u?.baseURL&&u.baseURL.includes("openrouter.ai")&&"openrouter"!==a&&(l.info(`[${this.currentSessionId}] piModelRef auto-correction: baseURL is openrouter.ai, switching provider from "${a}" to "openrouter" (modelId: "${a}/${h}")`),h=`${a}/${h}`,a="openrouter"),l.info(`[${this.currentSessionId}] piModelRef resolved: provider="${a}", modelId="${h}", contextWindow=${u?.contextWindow??128e3}`);const p=o?.rollingMemoryModel;if(p){const e=p.split(":");if(e.length>=3)c=e[1].trim(),d=e.slice(2).join(":").trim();else if(2===e.length){const s=e[1].indexOf("/");s>0?(c=e[1].substring(0,s).trim(),d=e[1].substring(s+1).trim()):d=e[1].trim()}d&&l.info(`[${this.currentSessionId}] Summarization model resolved: ${c}/${d}`)}return{provider:a,modelId:h,apiKey:u?.apiKey||void 0,baseUrl:u?.baseURL||void 0,contextWindowTokens:u?.contextWindow||void 0,costInput:u?.costInput||void 0,costOutput:u?.costOutput||void 0,costCacheRead:u?.costCacheRead||void 0,costCacheWrite:u?.costCacheWrite||void 0,summarizationProvider:c,summarizationModelId:d}}static API_RETRY_RE=/API Error:\s*(?:500|502|503|529)|overloaded|internal server error/i;async send(e,s){const t=s??0;if(this.closed||this.outputDone)throw new Error("SessionAgent is closed");if(this.skillNudgeEnabled&&this.toolCallsLastTurn>=this.skillNudgeThreshold&&0===t){const s=`[System note: Your previous turn used ${this.toolCallsLastTurn} tool calls. If you discovered a reusable workflow or improved on an existing skill's approach, consider updating or creating a skill (Edit the SKILL.md file directly). Skip this if the task was inherently complex and doesn't generalize.]`;e={...e,text:s+"\n\n"+e.text},this.toolCallsLastTurn=0,l.info(`[${this.sessionKey}] Skill nudge injected (previous turn had ${this.skillNudgeThreshold}+ tool calls)`)}let o;switch(this.loopDetector.reset(),this.ensureInitialized(),this.queueMode){case"collect":o=await this.sendCollect(e);break;case"steer":o=await this.sendSteer(e);break;default:o=await this.sendDirect(e)}const n=o.fullResponse??o.response,r=SessionAgent.API_RETRY_RE.test(n),a=this.config.agent.apiRetry,h=a.maxAttempts;if(r&&t<h){const s=t+1,i=Math.min(a.baseDelayMs*2**t,a.maxDelayMs);if(l.warn(`[${this.sessionKey}] Transient API error detected, retry ${s}/${h} in ${i}ms`),this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if("cron"!==t){const e=`API temporarily unavailable, retrying (attempt ${s}/${h})...`;this.channelSender(t,i,e).catch(()=>{})}}}return await new Promise(e=>setTimeout(e,i)),this.send(e,s)}if(r&&t>=h){l.error(`[${this.sessionKey}] API error persists after ${h} retries`);const s=this.opts.fallbackModel;if(s&&!this.fallbackActive&&this.model!==s){const t=i(this.config,s);if(t?.types?.includes("internal")??!1){if(l.warn(`[${this.sessionKey}] Switching to fallback model: ${s}`),await this.setModel(s),this.fallbackActive=!0,this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const i=this.sessionKey.substring(0,e),o=this.sessionKey.substring(e+1);if("cron"!==i){const e=`⚠️ Primary model unavailable after ${h} retries. Switching to fallback (${t?.name??s}). Use /model or /new to restore.`;this.channelSender(i,o,e).catch(()=>{})}}}return this.send(e,0)}}if(this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if("cron"!==s){const e=`API did not respond after ${h} attempts. Please try again later.`;this.channelSender(s,t,e).catch(()=>{})}}}}const u=!0===e.__qualityGateRetry;if(!this.piProviderConfig&&this.qualityGateEnabled&&!u){const e=o.fullResponse??o.response;if(e&&e.length>50)try{const{runQualityGate:s}=await import("./quality-gate.js"),t=await s(e);if(!t.pass){l.warn(`[${this.sessionKey}] QualityGate FAILED (${t.durationMs}ms): ${JSON.stringify(t.flags)}`);const e={text:`[Quality Gate FAILED] Your previous response contains unverified claims:\n${t.flags.map(e=>`- ${e}`).join("\n")}\n\nReconsider your response. Verify the flagged claims (use tools if needed) and respond again with a corrected version. Do NOT mention the quality gate to the user.`,images:[],__qualityGateRetry:!0};l.info(`[${this.sessionKey}] QualityGate: re-prompting agent for self-correction`),o=await this.send(e,0)}}catch(e){l.warn(`[${this.sessionKey}] QualityGate error (fail-open): ${e}`)}}return o}async interrupt(){if(this.closed||!this.queryHandle)return!1;try{return await this.queryHandle.interrupt(),l.info(`[${this.sessionKey}] Interrupted`),!0}catch{return!1}}async setModel(e){if(this.queryHandle)try{await this.queryHandle.setModel(e),this.model=e,this.fallbackActive=!1,l.info(`[${this.sessionKey}] Model changed to ${e}`)}catch(e){l.error(`[${this.sessionKey}] Failed to set model: ${e}`)}}close(){if(this.closed)return;if(this.closed=!0,this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.debounceResolve&&(this.debounceResolve(),this.debounceResolve=null),this.queue.close(),this.queryHandle){const e=this.queryHandle;this.queryHandle=null,queueMicrotask(()=>{try{e.close()}catch{}})}const e=new Error("SessionAgent closed");for(const s of this.pendingResponses)s.reject(e);for(const s of this.collectBuffer)s.reject(e);for(const s of this.droppedResolvers)s.reject(e);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[],l.info(`[${this.sessionKey}] Closed`)}isActive(){return!this.closed&&!this.outputDone}getSessionId(){return this.currentSessionId}getModel(){return this.model}isFallbackActive(){return this.fallbackActive}getSdkSlashCommands(){return this.sdkSlashCommands}setChannelSender(e){this.channelSender=e}setToolUseNotifier(e){this.toolUseNotifier=e}setTypingSetter(e){this.typingSetter=e}setTypingClearer(e){this.typingClearer=e}setTextBlockStreamer(e){this.textBlockStreamer=e}setUsageRecorder(e){this.usageRecorder=e}buildEnvForModel(e){const s=this.config.models.find(s=>s.id===e);if(!s?.proxy||"not-used"===s.proxy)return{env:{...process.env},proxied:!1,disableThinking:!1};const t={...process.env};return"direct"===s.proxy?(t.ANTHROPIC_BASE_URL=s.baseURL,t.ANTHROPIC_AUTH_TOKEN=s.apiKey,t.ANTHROPIC_API_KEY="",l.info(`[${this.sessionKey}] Direct env applied for model ${e} (url=${t.ANTHROPIC_BASE_URL})`),{env:t,proxied:!0,disableThinking:!1}):(t.ANTHROPIC_BASE_URL=s.fastUrl||this.config.fastProxyUrl,t.ANTHROPIC_AUTH_TOKEN=s.fastProxyApiKey,t.ANTHROPIC_API_KEY="",delete t.ANTHROPIC_BETAS,delete t.CLAUDE_CODE_EXTRA_BODY,l.info(`[${this.sessionKey}] Proxy env applied for model ${e} (url=${t.ANTHROPIC_BASE_URL})`),{env:t,proxied:!0,disableThinking:!0})}hasPendingPermission(){return null!==this.pendingPermission}resolvePermission(e){if(!this.pendingPermission)return;const s=this.pendingPermission;this.pendingPermission=null,e?(l.info(`[${this.sessionKey}] Permission approved: ${s.toolName}`),s.resolve({behavior:"allow",updatedInput:s.input})):(l.info(`[${this.sessionKey}] Permission denied: ${s.toolName}`),s.resolve({behavior:"deny",message:"User denied this action"}))}isBusy(){return this.pendingResponses.length>0}hasPendingQuestion(){return null!==this.pendingQuestion}resolveQuestion(e){if(!this.pendingQuestion)return;const s=this.pendingQuestion;this.pendingQuestion=null,l.info(`[${this.sessionKey}] Question answered: "${e}" for "${s.questionText}"`),s.resolve(e)}async handleCanUseTool(e,s){if("AskUserQuestion"===e){if(!this.channelSender)return l.warn(`[${this.sessionKey}] No channel sender for AskUserQuestion, auto-approving`),{behavior:"allow",updatedInput:s};const e=this.sessionKey.indexOf(":");if(e<0)return{behavior:"allow",updatedInput:s};const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if(!t||!i||"cron"===t)return{behavior:"allow",updatedInput:s};const o=s?.questions;if(!Array.isArray(o)||0===o.length)return{behavior:"allow",updatedInput:s};const n={};for(const e of o){const o=e.question||"?",r=Array.isArray(e.options)?e.options:[],a=[];if(e.header&&a.push(`*${e.header}*`),a.push(o),r.some(e=>e.description)){a.push("");for(const e of r){const s=e.description?`: ${e.description}`:"";a.push(`• ${e.label}${s}`)}}const h=a.join("\n");if(this.typingClearer)try{await this.typingClearer(t,i)}catch{}try{if(r.length>0){const e=r.map(e=>({text:e.label||String(e),callbackData:`__ask:${e.label||String(e)}`}));await this.channelSender(t,i,h,e)}else await this.channelSender(t,i,h)}catch(e){return l.error(`[${this.sessionKey}] Failed to send AskUserQuestion: ${e}`),{behavior:"allow",updatedInput:s}}const u=55e3,c=await new Promise(e=>{const s=setTimeout(()=>{if(this.pendingQuestion){this.pendingQuestion=null;const s=r.length>0?r[0].label||String(r[0]):"No answer";l.warn(`[${this.sessionKey}] Question timeout, defaulting to "${s}"`),this.channelSender&&this.channelSender(t,i,`[Timeout] Auto-selected: ${s}`).catch(()=>{}),e(s)}},u);this.pendingQuestion={resolve:t=>{clearTimeout(s),e(t)},questionText:o}});if(n[o]=c,this.typingSetter)try{await this.typingSetter(t,i)}catch{}}return l.info(`[${this.sessionKey}] AskUserQuestion answered: ${JSON.stringify(n)}`),{behavior:"allow",updatedInput:{questions:s.questions,answers:n}}}const t=this.loopDetector.check(e,s);if("circuit_break"===t.severity)return l.error(`[${this.sessionKey}] CIRCUIT BREAK: ${t.reason}`),{behavior:"deny",message:`[Loop detected] ${t.reason} Try a different approach or ask the user for help.`};if(this.autoApproveTools)return l.debug(`[${this.sessionKey}] Auto-approving tool: ${e}`),{behavior:"allow",updatedInput:s};if(!this.channelSender)return l.warn(`[${this.sessionKey}] No channel sender for interactive permission, auto-approving: ${e}`),{behavior:"allow",updatedInput:s};const i=this.sessionKey.indexOf(":");if(i<0)return{behavior:"allow",updatedInput:s};const o=this.sessionKey.substring(0,i),n=this.sessionKey.substring(i+1);if(!o||!n||"cron"===o)return{behavior:"allow",updatedInput:s};const r=[`[Permission Request] Tool: ${e}`];if("Bash"===e&&s?.command)r.push(`Command: ${s.command}`),s.description&&r.push(`Description: ${s.description}`);else if("Write"===e&&s?.file_path)r.push(`File: ${s.file_path}`);else if("Edit"===e&&s?.file_path)r.push(`File: ${s.file_path}`);else if("ExitPlanMode"===e&&s?.plan){if(r.push(""),r.push(s.plan),Array.isArray(s.allowedPrompts)&&s.allowedPrompts.length>0){r.push(""),r.push("Requested permissions:");for(const e of s.allowedPrompts)r.push(` - [${e.tool}] ${e.prompt}`)}}else{const e=JSON.stringify(s);e.length<=300?r.push(`Input: ${e}`):r.push(`Input: ${e.slice(0,297)}...`)}r.push(""),r.push("Reply: approve to allow, deny to reject");const a=r.join("\n"),h=[{text:"Approve",callbackData:"__tool_perm:approve"},{text:"Deny",callbackData:"__tool_perm:deny"}];try{await this.channelSender(o,n,a,h)}catch(e){return l.error(`[${this.sessionKey}] Failed to send permission request: ${e}`),{behavior:"allow",updatedInput:s}}if(this.typingClearer)try{await this.typingClearer(o,n)}catch{}const u=12e4;return new Promise(t=>{const i=setTimeout(()=>{this.pendingPermission?.resolve===t&&(this.pendingPermission=null,l.warn(`[${this.sessionKey}] Permission timeout for ${e}, auto-denying`),this.channelSender&&this.channelSender(o,n,`[Permission timeout] Tool ${e} denied after 120s`).catch(()=>{}),t({behavior:"deny",message:"Permission request timed out"}))},u);this.pendingPermission={resolve:t,toolName:e,input:s};const r=t;this.pendingPermission.resolve=e=>{clearTimeout(i),r(e)}})}async forwardAskUserQuestion(e){if(!this.channelSender)return;const s=this.sessionKey.indexOf(":");if(s<0)return;const t=this.sessionKey.substring(0,s),i=this.sessionKey.substring(s+1);if(!t||!i||"cron"===t)return;const o=e?.questions;if(Array.isArray(o)){for(const e of o){const s=e.question||"?",o=Array.isArray(e.options)?e.options:[],n=[];if(e.header&&n.push(`*${e.header}*`),n.push(s),o.some(e=>e.description)){n.push("");for(const e of o){const s=e.description?`: ${e.description}`:"";n.push(`• ${e.label}${s}`)}}const r=n.join("\n");try{if(o.length>0){const e=o.map(e=>({text:e.label||String(e),callbackData:e.label||String(e)}));await this.channelSender(t,i,r,e)}else await this.channelSender(t,i,r)}catch(e){l.error(`[${this.sessionKey}] Failed to forward AskUserQuestion: ${e}`)}}if(this.typingClearer)try{await this.typingClearer(t,i)}catch(e){l.error(`[${this.sessionKey}] Failed to clear typing: ${e}`)}}}async notifyToolUse(e){if(!this.toolUseNotifier)return;const s=this.sessionKey.indexOf(":");if(s<0)return;const t=this.sessionKey.substring(0,s),i=this.sessionKey.substring(s+1);if(t&&i&&"cron"!==t)try{await this.toolUseNotifier(t,i,e)}catch(e){l.error(`[${this.sessionKey}] Failed to notify tool use: ${e}`)}}async flushPendingTextBlock(){if(!this.textBlockStreamer||!this.pendingTextBlock)return;const e=this.sessionKey.indexOf(":");if(e<0)return;const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if(!s||!t||"cron"===s)return;const i=this.pendingTextBlock;this.pendingTextBlock="",this.streamedAny=!0,this.streamedText+=i;try{await this.textBlockStreamer(s,t,i)}catch(e){l.error(`[${this.sessionKey}] Text block stream error: ${e}`)}}sendDirect(e){if(this.queueCap>0&&this.pendingResponses.length>=this.queueCap)return l.warn(`[${this.sessionKey}] Queue cap reached (${this.queueCap}), rejecting message`),Promise.resolve({response:"Queue is full. Please wait for the current processing to complete.",sessionId:this.currentSessionId,sessionReset:!1});const s=this.buildQueueMessage(e);return new Promise((e,t)=>{this.pendingResponses.push({resolve:e,reject:t}),this.queue.push(s),l.info(`[${this.sessionKey}] Message queued (pending=${this.pendingResponses.length})`)})}sendCollect(e){return this.pendingResponses.length>0?this.queueCap>0&&this.collectBuffer.length>=this.queueCap?this.applyDropPolicy(e):(this.lastCollectAt=Date.now(),l.info(`[${this.sessionKey}] Collecting message (buffer=${this.collectBuffer.length+1})`),new Promise((s,t)=>{this.collectBuffer.push({prompt:e,resolve:s,reject:t})})):this.sendDirect(e)}async sendSteer(e){return this.pendingResponses.length>0&&(l.info(`[${this.sessionKey}] Steer: interrupting current processing`),await this.interrupt()),this.sendDirect(e)}applyDropPolicy(e){if("new"===this.dropPolicy)return l.warn(`[${this.sessionKey}] Queue cap reached, rejecting new message`),Promise.resolve({response:"Queue is full. Please wait for the current processing to complete.",sessionId:this.currentSessionId,sessionReset:!1});const s=this.collectBuffer.shift();return"summarize"===this.dropPolicy&&this.droppedSummaries.push(function(e,s){const t=e.replace(/\s+/g," ").trim();return t.length<=s?t:`${t.slice(0,s-1).trimEnd()}…`}(s.prompt.text,140)),this.droppedResolvers.push({resolve:s.resolve,reject:s.reject}),l.warn(`[${this.sessionKey}] Queue cap reached, dropped oldest message (policy=${this.dropPolicy}, dropped=${this.droppedResolvers.length})`),this.lastCollectAt=Date.now(),new Promise((s,t)=>{this.collectBuffer.push({prompt:e,resolve:s,reject:t})})}async debounceThenFlush(){if(this.debounceMs<=0||this.closed)this.flushCollectBuffer();else{for(;!this.closed&&this.collectBuffer.length>0;){const e=Date.now()-this.lastCollectAt;if(e>=this.debounceMs)break;await new Promise(s=>{this.debounceResolve=s,this.debounceTimer=setTimeout(s,this.debounceMs-e)}),this.debounceTimer=null,this.debounceResolve=null}this.closed||this.flushCollectBuffer()}}flushCollectBuffer(){if(0===this.collectBuffer.length&&0===this.droppedResolvers.length)return;const e=this.collectBuffer.splice(0),s=e.map(e=>e.prompt),t=this.mergePrompts(s),i=this.buildQueueMessage(t),o=[...this.droppedResolvers.splice(0),...e.map(e=>({resolve:e.resolve,reject:e.reject}))];this.droppedSummaries=[],this.pendingResponses.push({resolve:e=>{for(const s of o)s.resolve(e)},reject:e=>{for(const s of o)s.reject(e)}}),this.queue.push(i),l.info(`[${this.sessionKey}] Flushed ${e.length} collected message(s) as one prompt`)}mergePrompts(e){const s=[],t=[];if(this.droppedSummaries.length>0){s.push(`[${this.droppedSummaries.length} earlier message(s) dropped due to queue cap]`);for(const e of this.droppedSummaries)s.push(`- ${e}`);s.push("")}if(1===e.length&&0===this.droppedSummaries.length)return e[0];if(e.length>0){s.push("[Queued messages while agent was busy]");for(let i=0;i<e.length;i++)e[i].text&&s.push(`${i+1}. ${e[i].text}`),t.push(...e[i].images)}return{text:s.join("\n"),images:t}}ensureInitialized(){if(this.initialized)return;this.initialized=!0;const s=this.piProviderConfig?"pi":"claudecode";l.info(`[${this.sessionKey}] Starting agent: engine=${s}, model=${this.model}, mode=${this.queueMode}, debounce=${this.debounceMs}ms, cap=${this.queueCap||"unlimited"}, drop=${this.dropPolicy}, session=${this.currentSessionId||"new"}`),this.piProviderConfig?this.initPiEngine():(this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput())}async initPiEngine(){try{const e=await import("../pi-agent-provider/index.js"),s=await e.createToolRegistryFromOptions(this.opts);this.queryHandle=e.piQuery({prompt:this.queue,options:this.opts},this.piProviderConfig,s),this.processOutput(),l.info(`[${this.sessionKey}] Pi engine initialized: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}catch(s){l.error(`[${this.sessionKey}] Failed to initialize Pi engine: ${s}`),l.warn(`[${this.sessionKey}] Falling back to Claude SDK (claudecode engine)`),this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput()}}buildQueueMessage(e){if(0===e.images.length)return l.debug(`[${this.sessionKey}] SDK request: text-only (${e.text.length} chars): ${this.config.verboseDebugLogs?e.text:e.text.slice(0,15)+"..."}`),{type:"user",message:{role:"user",content:e.text}};const s=[];for(const t of e.images)s.push({type:"image",source:{type:"base64",media_type:t.mimeType,data:t.base64}});return e.text&&s.push({type:"text",text:e.text}),l.debug(`[${this.sessionKey}] SDK request: ${s.length} block(s) [${s.map(e=>"image"===e.type?`image/${e.source.media_type}`:`text(${e.text?.length??0})`).join(", ")}]`),{type:"user",message:{role:"user",content:s}}}async processOutput(){if(this.queryHandle)try{for await(const e of this.queryHandle){if(this.closed)break;if(l.debug(`[${this.sessionKey}] SDK message: type=${e.type}, subtype=${e.subtype??"-"}, keys=${Object.keys(e).join(",")}`),"system"===e.type){const s=e,t=s.subtype;if("init"===t){const e=s.slash_commands;Array.isArray(e)&&(this.sdkSlashCommands=e.map(e=>e.replace(/^\//,"")))}if("compact_boundary"===t){const e=s.compact_metadata,t=["Context compacted."];e?.pre_tokens&&t.push(`Pre-compaction tokens: ${e.pre_tokens}.`),e?.trigger&&t.push(`Trigger: ${e.trigger}.`),this.currentResponse=t.join(" "),l.info(`[${this.sessionKey}] Compact: ${this.currentResponse}`)}else if("init"!==t&&"status"!==t){const e=new Set(["task_started","task_notification","files_persisted","hook_started","hook_progress","hook_response"]),{type:i,...o}=s,n=JSON.stringify(o,null,2);e.has(t)?l.debug(`[${this.sessionKey}] Internal SDK event (${t}): ${n.slice(0,200)}`):(this.currentResponse=n,l.info(`[${this.sessionKey}] System message (${t??"unknown"}): ${n.slice(0,200)}`))}}if("assistant"===e.type){const s=e.message.content,t=s.filter(e=>"text"===e.type).map(e=>e.text).join("");t&&(this.currentResponse=t,this.pendingTextBlock=t);const i=s.map(e=>e.type).join(", ");l.debug(`[${this.sessionKey}] SDK assistant message: blocks=[${i}], text length=${t.length}: ${this.config.verboseDebugLogs?t:t.slice(0,15)+"..."}`);s.some(e=>"tool_use"===e.type)&&this.pendingTextBlock&&this.textBlockStreamer&&!this.qualityGateEnabled&&await this.flushPendingTextBlock();for(const e of s)if("tool_use"===e.type){this.toolCallsCurrentTurn++,"mcp__message-tools__send_message"===e.name&&(this.messageSentViaTool=!0);const s=JSON.stringify(e.input);l.debug(`[${this.sessionKey}] Tool call: ${e.name} ${this.config.verboseDebugLogs?s:s.slice(0,100)+(s.length>100?"...":"")}`),this.toolUseNotifier&&"AskUserQuestion"!==e.name&&await this.notifyToolUse(e.name)}}if("tool_progress"===e.type){const s=e;l.debug(`[${this.sessionKey}] Tool progress: ${s.tool_name} (${s.elapsed_time_seconds}s)`)}if("result"===e.type){const s=e;let t;l.debug(`[${this.sessionKey}] SDK result: subtype=${s.subtype}, stop_reason=${s.stop_reason??"null"}, session=${s.session_id??"n/a"}, result length=${s.result?.length??0}`),"session_id"in s&&(this.currentSessionId=s.session_id),this.usageRecorder&&(void 0!==s.total_cost_usd||s.modelUsage)&&this.usageRecorder(this.sessionKey,s.total_cost_usd,s.duration_ms,s.num_turns,s.modelUsage);const i=s.stop_reason??null;if("success"===s.subtype){if(s.result)this.currentResponse=s.result;else if(!this.currentResponse&&!this.messageSentViaTool&&this.pendingResponses.length<=1&&(void 0!==s.total_cost_usd||s.usage)){const e=[];if(void 0!==s.total_cost_usd&&e.push(`Total cost: $${Number(s.total_cost_usd).toFixed(4)}`),void 0!==s.duration_ms&&e.push(`Duration: ${(s.duration_ms/1e3).toFixed(1)}s`),void 0!==s.num_turns&&e.push(`Turns: ${s.num_turns}`),s.modelUsage)for(const[t,i]of Object.entries(s.modelUsage)){const s=i,o=[` ${t}:`];s.inputTokens&&o.push(`input=${s.inputTokens}`),s.outputTokens&&o.push(`output=${s.outputTokens}`),s.cacheReadInputTokens&&o.push(`cache_read=${s.cacheReadInputTokens}`),s.cacheCreationInputTokens&&o.push(`cache_create=${s.cacheCreationInputTokens}`),void 0!==s.costUSD&&o.push(`cost=$${Number(s.costUSD).toFixed(4)}`),e.push(o.join(" "))}e.length>0&&(this.currentResponse=e.join("\n"))}if(!s.result&&!this.currentResponse&&this.pendingResponses.length<=1){const e=this.piProviderConfig;l.warn(`[${this.sessionKey}] Empty response on success: provider=${e?.provider??"sdk"}, modelId=${e?.modelId??"n/a"}, stop_reason=${i}. Check provider routing and API key.`)}"refusal"===i?(l.warn(`[${this.sessionKey}] Model refused the request`),this.currentResponse||(this.currentResponse="I'm unable to fulfill this request.")):"error"===i?(l.warn(`[${this.sessionKey}] Model returned stop_reason=error`),this.currentResponse||(this.currentResponse="⚠️ Request failed. The provider may be unavailable or the API key/credits may be exhausted.")):"max_tokens"===i&&l.warn(`[${this.sessionKey}] Response truncated: output token limit reached`)}else if("error_max_turns"===s.subtype)t="max_turns",l.warn(`[${this.sessionKey}] Max turns reached`);else if("error_max_budget_usd"===s.subtype)t="max_budget",l.warn(`[${this.sessionKey}] Max budget reached`);else{const e=s.errors??[];if(e.some(e=>e.includes("aborted")))l.info(`[${this.sessionKey}] Request aborted (steer interrupt)`);else if(l.error(`[${this.sessionKey}] SDK error: ${JSON.stringify(s)}`),!this.currentResponse&&!this.streamedAny){const s=e.filter(e=>!e.includes("aborted")).join("; ");this.currentResponse="⚠️ Request failed"+(s?`: ${s}`:". The provider may be unavailable or the API key/credits may be exhausted.")}}const o=this.pendingResponses.shift();if(o){const e=this.currentResponse||"";let s=e;this.streamedAny&&(s=this.pendingTextBlock||"",!s&&e&&e.length>this.streamedText.length&&(s=e.startsWith(this.streamedText)?e.slice(this.streamedText.length).replace(/^\n+/,""):e)),l.info(`[${this.sessionKey}] Response ready: session=${this.currentSessionId}, length=${s.length}${this.streamedAny?` (streamed, full=${e.length})`:""}`),o.resolve({response:s,fullResponse:this.streamedAny?e:void 0,sessionId:this.currentSessionId,sessionReset:!1,errorType:t,stopReason:i})}this.toolCallsLastTurn=this.toolCallsCurrentTurn,this.toolCallsCurrentTurn=0,this.messageSentViaTool=!1,this.currentResponse="",this.pendingTextBlock="",this.streamedAny=!1,this.streamedText="","collect"===this.queueMode&&(this.collectBuffer.length>0||this.droppedResolvers.length>0)&&await this.debounceThenFlush()}}}catch(e){l.error(`[${this.sessionKey}] Output stream error: ${e}`);const s=this.pendingResponses.shift();if(s)if(this.currentSessionId){const t=e instanceof Error?e.message:String(e);l.warn(`[${this.sessionKey}] Session error (${this.currentSessionId}): ${t}`),s.resolve({response:t,sessionId:"",sessionReset:!0})}else s.reject(e instanceof Error?e:new Error(String(e)));const t=new Error("SessionAgent terminated");for(const e of this.pendingResponses)e.reject(t);for(const e of this.collectBuffer)e.reject(t);for(const e of this.droppedResolvers)e.reject(t);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[]}finally{this.outputDone=!0}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFileSync as e,copyFileSync as t,existsSync as n,mkdirSync as o}from"node:fs";import{join as r}from"node:path";import{createLogger as s}from"../utils/logger.js";import{resolveInstallPkgDir as a}from"../utils/package-paths.js";const c=s("WorkspaceFiles"),l=[
|
|
1
|
+
import{readFileSync as e,copyFileSync as t,existsSync as n,mkdirSync as o}from"node:fs";import{join as r}from"node:path";import{createLogger as s}from"../utils/logger.js";import{resolveInstallPkgDir as a}from"../utils/package-paths.js";const c=s("WorkspaceFiles"),l=[],i=["SYSTEM_PROMPT.md","SYSTEM_PROMPT_SUBAGENT.md","CBINT.json"],u=["MEMORY.md","memory.md"],f=new Set,m=8e3;export function ensureWorkspaceFiles(e){o(e,{recursive:!0});const s=g(),a=r(e,".templates");o(a,{recursive:!0});for(const o of l){const a=r(e,o);if(!n(a)){const e=r(s,o);n(e)&&(t(e,a),c.info(`Seeded ${o} → ${a}`))}}for(const e of i){const o=r(a,e);if(!n(o)){const a=r(s,e);n(a)&&(t(a,o),c.info(`Seeded template ${e} → ${o}`))}}}export function loadWorkspaceFiles(t){const o=[];for(const s of l){const a=r(t,s);if(n(a))try{const t=e(a,"utf-8");o.push({name:s,path:a,content:t,missing:!1})}catch(e){c.warn(`Failed to read ${a}: ${e}`),o.push({name:s,path:a,content:"",missing:!0})}}for(const s of u){const a=r(t,s);if(n(a)){try{const t=e(a,"utf-8");o.push({name:s,path:a,content:t,missing:!1})}catch(e){c.warn(`Failed to read ${a}: ${e}`)}break}}return o}export function filterForSubagent(e){return e.filter(e=>f.has(e.name))}export function truncateContent(e,t,n=2e4){if(e.length<=n)return e;const o=Math.floor(.7*n),r=Math.floor(.2*n),s=e.slice(0,o),a=e.slice(-r),l=e.length-o-r;return c.debug(`Truncated ${t}: ${e.length} → ${o+r} chars (skipped ${l})`),`${s}\n\n[... ${l} characters omitted from ${t} ...]\n\n${a}`}const d=new Set(["HEARTBEAT.md","BOOTSTRAP.md"]),h=/\{\{FILE:([A-Za-z0-9_\-]+\.md)\}\}/g;export function formatWorkspaceFiles(t,o){const s=t.filter(e=>!e.missing&&e.content.trim().length>0&&!d.has(e.name));return 0===s.length?"(No workspace files found)":s.map(t=>{let s=t.content;return o&&s.includes("{{FILE:")&&(s=function(t,o){return t.replace(h,(t,s)=>{const a=r(o,s);if(!n(a))return c.warn(`File include not found: {{FILE:${s}}} → ${a}`),`\x3c!-- FILE:${s} not found --\x3e`;try{const t=e(a,"utf-8");return c.info(`Resolved {{FILE:${s}}} (${t.length} chars)`),t}catch(e){return c.warn(`Failed to read included file ${s}: ${e}`),`\x3c!-- FILE:${s} read error --\x3e`}})}(s,o)),p(t.name)&&s.length>m&&(s=extractHotSections(s,t.name)),truncateContent(s,t.name)}).join("\n\n")}function p(e){return u.includes(e)}export function extractHotSections(e,t){const n="\x3c!-- hot --\x3e",o="\x3c!-- /hot --\x3e",r=[];let s=0;for(;s<e.length;){const t=e.indexOf(n,s);if(-1===t)break;const a=t+12,c=e.indexOf(o,a);if(-1===c){r.push(e.slice(a).trim());break}r.push(e.slice(a,c).trim()),s=c+13}if(0===r.length)return e;const a=r.join("\n\n"),l=e.length-a.length;return c.info(`Memory budget: ${t} ${e.length} chars → ${a.length} hot, ${l} cold (via memory_search)`),`${a}\n\n[... ${l} characters of cold memory omitted from ${t} — available via memory_search ...]`}export function scanFileIncludes(e){const t=[];for(const n of e){if(n.missing||!n.content)continue;let e;const o=/\{\{FILE:([A-Za-z0-9_\-]+\.md)\}\}/g;for(;null!==(e=o.exec(n.content));)t.push({fileName:e[1],sourceFile:n.name})}return t}export function formatWorkspaceFilesWithMeta(t,o){const s={fileIncludes:[],memoryBudget:null,truncations:[],loadedFiles:[]},a=t.filter(e=>!e.missing&&e.content.trim().length>0&&!d.has(e.name));for(const e of t)s.loadedFiles.push({name:e.name,chars:e.content?.length??0,excluded:e.missing||d.has(e.name)||!e.content?.trim()});if(0===a.length)return{formatted:"(No workspace files found)",meta:s};return{formatted:a.map(t=>{let a=t.content;if(o&&a.includes("{{FILE:")&&(a=a.replace(h,(a,c)=>{const l=r(o,c);if(!n(l))return s.fileIncludes.push({placeholder:`{{FILE:${c}}}`,fileName:c,resolved:!1,chars:0,sourceFile:t.name}),`\x3c!-- FILE:${c} not found --\x3e`;try{const n=e(l,"utf-8");return s.fileIncludes.push({placeholder:`{{FILE:${c}}}`,fileName:c,resolved:!0,chars:n.length,sourceFile:t.name}),n}catch{return s.fileIncludes.push({placeholder:`{{FILE:${c}}}`,fileName:c,resolved:!1,chars:0,sourceFile:t.name}),`\x3c!-- FILE:${c} read error --\x3e`}})),p(t.name)){const e=a.includes("\x3c!-- hot --\x3e"),n=a.length>m;if(n&&e){const o=a.length;a=extractHotSections(a,t.name);const r=a.lastIndexOf("[..."),c=r>=0?r:a.length;s.memoryBudget={fileName:t.name,totalChars:o,hotChars:c,coldChars:o-c,hasHotMarkers:e,budgetThreshold:m,budgetExceeded:n}}else s.memoryBudget={fileName:t.name,totalChars:a.length,hotChars:a.length,coldChars:0,hasHotMarkers:e,budgetThreshold:m,budgetExceeded:n}}if(a.length>2e4){const e=a.length;a=truncateContent(a,t.name),s.truncations.push({fileName:t.name,originalChars:e,truncatedChars:a.length,skippedChars:e-a.length})}return a}).join("\n\n"),meta:s}}export function loadBuiltInTools(e,t){const n=loadTemplateOrEmpty(e,"CBINT.json");if(!n.trim())return"";try{const e=JSON.parse(n),o="full"===t?{...e.tools_main_agent||{},...e.tools_sub_agent||{}}:e.tools_sub_agent||{},r=Object.entries(o);if(0===r.length)return"";const s=[];for(const[e,t]of r)s.push(`- **${e}**: ${t}`);return s.join("\n")}catch(e){return c.warn(`Failed to parse CBINT.json: ${e}`),""}}export function loadTemplateOrEmpty(t,o){const s=r(t,".templates",o);if(n(s))try{return e(s,"utf-8")}catch{return""}const a=r(g(),o);if(n(a))try{return e(a,"utf-8")}catch{return""}return""}export function loadTemplate(t,o){const s=r(t,".templates",o);if(n(s))return e(s,"utf-8");const a=r(g(),o);if(n(a))return c.warn(`Template ${o} not found in ${t}/.templates/, using bundled seed`),e(a,"utf-8");throw new Error(`Template ${o} not found in ${t}/.templates/ or bundled seeds`)}function g(){return a()}
|
package/dist/config.d.ts
CHANGED
|
@@ -79,6 +79,7 @@ declare const TelegramAccountSchema: z.ZodObject<{
|
|
|
79
79
|
}, z.core.$strip>>;
|
|
80
80
|
timeoutSeconds: z.ZodOptional<z.ZodNumber>;
|
|
81
81
|
proxy: z.ZodOptional<z.ZodString>;
|
|
82
|
+
inputDebounceMs: z.ZodDefault<z.ZodNumber>;
|
|
82
83
|
}, z.core.$strip>;
|
|
83
84
|
declare const TelegramConfigSchema: z.ZodObject<{
|
|
84
85
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -131,6 +132,7 @@ declare const TelegramConfigSchema: z.ZodObject<{
|
|
|
131
132
|
}, z.core.$strip>>;
|
|
132
133
|
timeoutSeconds: z.ZodOptional<z.ZodNumber>;
|
|
133
134
|
proxy: z.ZodOptional<z.ZodString>;
|
|
135
|
+
inputDebounceMs: z.ZodDefault<z.ZodNumber>;
|
|
134
136
|
}, z.core.$strip>>>;
|
|
135
137
|
}, z.core.$strip>;
|
|
136
138
|
export type TelegramActionConfig = z.infer<typeof TelegramActionConfigSchema>;
|
|
@@ -232,6 +234,7 @@ declare const AppConfigSchema: z.ZodObject<{
|
|
|
232
234
|
}, z.core.$strip>>;
|
|
233
235
|
timeoutSeconds: z.ZodOptional<z.ZodNumber>;
|
|
234
236
|
proxy: z.ZodOptional<z.ZodString>;
|
|
237
|
+
inputDebounceMs: z.ZodDefault<z.ZodNumber>;
|
|
235
238
|
}, z.core.$strip>>>;
|
|
236
239
|
}, z.core.$strip>>>;
|
|
237
240
|
whatsapp: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
@@ -272,6 +275,10 @@ declare const AppConfigSchema: z.ZodObject<{
|
|
|
272
275
|
agentId: z.ZodDefault<z.ZodString>;
|
|
273
276
|
privateKey: z.ZodDefault<z.ZodString>;
|
|
274
277
|
reconnectDelayMs: z.ZodDefault<z.ZodNumber>;
|
|
278
|
+
rateLimit: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
279
|
+
maxPerPair: z.ZodDefault<z.ZodNumber>;
|
|
280
|
+
windowMs: z.ZodDefault<z.ZodNumber>;
|
|
281
|
+
}, z.core.$strip>>>;
|
|
275
282
|
}, z.core.$strip>>>;
|
|
276
283
|
responses: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
277
284
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -445,11 +452,16 @@ declare const AppConfigSchema: z.ZodObject<{
|
|
|
445
452
|
qualityGate: z.ZodDefault<z.ZodObject<{
|
|
446
453
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
447
454
|
}, z.core.$strip>>;
|
|
455
|
+
compaction: z.ZodOptional<z.ZodObject<{
|
|
456
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
457
|
+
contextTokenThreshold: z.ZodOptional<z.ZodNumber>;
|
|
458
|
+
}, z.core.$strip>>;
|
|
448
459
|
}, z.core.$strip>>>;
|
|
449
460
|
cron: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
450
461
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
451
462
|
isolated: z.ZodDefault<z.ZodBoolean>;
|
|
452
463
|
broadcastEvents: z.ZodDefault<z.ZodBoolean>;
|
|
464
|
+
trackMemory: z.ZodDefault<z.ZodBoolean>;
|
|
453
465
|
storePath: z.ZodDefault<z.ZodString>;
|
|
454
466
|
heartbeat: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
455
467
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -481,6 +493,15 @@ declare const AppConfigSchema: z.ZodObject<{
|
|
|
481
493
|
color: z.ZodDefault<z.ZodString>;
|
|
482
494
|
}, z.core.$strip>>>;
|
|
483
495
|
}, z.core.$strip>>>;
|
|
496
|
+
warmRestart: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
497
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
498
|
+
pm2Name: z.ZodDefault<z.ZodString>;
|
|
499
|
+
}, z.core.$strip>>>;
|
|
500
|
+
ckr: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
501
|
+
l2Prefix: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
502
|
+
applyL2PrefixOnEachUserMessage: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
503
|
+
removeL2PrefixFromMemory: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
504
|
+
}, z.core.$strip>>>;
|
|
484
505
|
}, z.core.$strip>;
|
|
485
506
|
type RawAppConfig = z.infer<typeof AppConfigSchema>;
|
|
486
507
|
export type AppConfig = RawAppConfig & {
|
package/dist/config.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFileSync as e,writeFileSync as t,existsSync as a,mkdirSync as o,renameSync as n,unlinkSync as l}from"node:fs";import{resolve as r,join as s}from"node:path";import{homedir as i}from"node:os";import{config as d}from"dotenv";import{parse as u,stringify as c}from"yaml";import{z as f}from"zod";import{BrowserConfigSchema as p}from"@hera-al/browser-server/config";import{createLogger as m}from"./utils/logger.js";const b=m("Config");function g(e){return"~"===e||e.startsWith("~/")?e.replace("~",i()):e}let h=g(process.env.GMAB_PATH??"~/gmab"),y=s(h,"data");export function getGmabPath(){return h}export function getDataDir(){return y}export function getNostromoKeyPath(){return s(y,".nostromo-key")}export function backupConfig(o){if(a(o))try{const r=e=>`${o}.backup${e}`;a(r(1))&&l(r(1));for(let e=2;e<=5;e++)a(r(e))&&n(r(e),r(e-1));t(r(5),e(o)),b.debug(`Config backup created: ${r(5)}`)}catch(e){b.warn(`Failed to create config backup: ${e}`)}}const v=f.record(f.string(),f.any()),x=f.object({reactions:f.boolean().default(!0),sendMessage:f.boolean().default(!0),editMessage:f.boolean().default(!0),deleteMessage:f.boolean().default(!0),sticker:f.boolean().default(!1),createForumTopic:f.boolean().default(!1)}),w=f.object({maxAttempts:f.number().default(3),baseDelayMs:f.number().default(1e3),maxDelayMs:f.number().default(3e4)}),M=f.enum(["off","dm","group","all","allowlist"]),k=f.object({botToken:f.string(),dmPolicy:f.enum(["open","token","allowlist"]).default("allowlist"),allowFrom:f.array(f.union([f.string(),f.number()])).default([]),name:f.string().optional(),reactionLevel:f.enum(["off","ack","minimal","extensive"]).default("ack"),reactionNotifications:f.enum(["off","own","all"]).default("off"),inlineButtonsScope:M.optional(),textChunkLimit:f.number().default(4e3),streamMode:f.enum(["off","partial","block"]).default("partial"),linkPreview:f.boolean().default(!0),actions:x.optional(),retry:w.optional(),timeoutSeconds:f.number().optional(),proxy:f.string().optional()}),j=f.object({enabled:f.boolean().default(!1),accounts:f.record(f.string(),k).default({})}),P=f.object({enabled:f.boolean().default(!1),accounts:v.default({})}),R=f.object({enabled:f.boolean().default(!0),port:f.number().default(3004)}),C=f.object({enabled:f.boolean().default(!1),brokerUrl:f.string().default("ws://127.0.0.1:3780/ws"),agentId:f.string().default(""),privateKey:f.string().default(""),reconnectDelayMs:f.number().default(5e3)}),S=f.object({telegram:j.optional().default({enabled:!1,accounts:{}}),whatsapp:P.optional().default({enabled:!1,accounts:{}}),discord:P.optional().default({enabled:!1,accounts:{}}),slack:P.optional().default({enabled:!1,accounts:{}}),signal:P.optional().default({enabled:!1,accounts:{}}),msteams:P.optional().default({enabled:!1,accounts:{}}),googlechat:P.optional().default({enabled:!1,accounts:{}}),line:P.optional().default({enabled:!1,accounts:{}}),matrix:P.optional().default({enabled:!1,accounts:{}}),mesh:C.optional().default({enabled:!1,brokerUrl:"ws://127.0.0.1:3780/ws",agentId:"",privateKey:"",reconnectDelayMs:5e3}),responses:R.optional().default({enabled:!0,port:3e3})}),T=f.object({modelRef:f.string().default(""),model:f.string().default("whisper-1"),language:f.string().default("")}),A=f.object({binaryPath:f.string().default("whisper"),model:f.string().default("base")}),D=f.object({enabled:f.boolean().default(!1),provider:f.string().default("openai-whisper"),"openai-whisper":T.optional().default({modelRef:"",model:"whisper-1",language:""}),"local-whisper":A.optional().default({binaryPath:"whisper",model:"base"})}),I=f.object({voice:f.string().default("en-US-MichelleNeural"),lang:f.string().default("en-US"),outputFormat:f.string().default("audio-24khz-48kbitrate-mono-mp3")}),U=f.object({modelRef:f.string().default(""),model:f.string().default("gpt-4o-mini-tts"),voice:f.string().default("alloy")}),z=f.object({modelRef:f.string().default(""),voiceId:f.string().default("pMsXgVXv3BLzUgSXRplE"),modelId:f.string().default("eleven_multilingual_v2")}),L=f.object({enabled:f.boolean().default(!1),provider:f.enum(["edge","openai","elevenlabs"]).default("openai"),maxTextLength:f.number().default(4096),timeoutMs:f.number().default(3e4),edge:I.optional().default({voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"}),openai:U.optional().default({modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"}),elevenlabs:z.optional().default({modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"})}),W=f.object({enabled:f.boolean().default(!1),embeddingModel:f.string().default("text-embedding-3-small"),embeddingDimensions:f.number().default(1536),modelRef:f.string().default(""),prefixQuery:f.string().default(""),prefixDocument:f.string().default(""),updateDebounceMs:f.number().default(3e3),embedIntervalMs:f.number().default(3e5),maxResults:f.number().default(6),maxSnippetChars:f.number().default(700),maxInjectedChars:f.number().default(4e3),rrfK:f.number().default(60)}),K={enabled:!1,embeddingModel:"text-embedding-3-small",embeddingDimensions:1536,modelRef:"",prefixQuery:"",prefixDocument:"",updateDebounceMs:3e3,embedIntervalMs:3e5,maxResults:6,maxSnippetChars:700,maxInjectedChars:4e3,rrfK:60},E=f.object({enabled:f.boolean().default(!0),model:f.string().default("")}),F={enabled:!0,model:""},q=f.object({enabled:f.boolean().default(!0),recallStrategy:f.enum(["builtin-only","search"]).default("builtin-only"),search:W.optional().default(K),l0:E.optional().default(F)}),G=f.object({command:f.string(),args:f.array(f.string()).optional(),env:f.record(f.string(),f.string()).optional()}).passthrough(),O=f.object({id:f.string(),name:f.string(),types:f.array(f.enum(["internal","external","env-var"])).optional().default(["external"]),proxy:f.enum(["not-used","direct","proxied"]).optional().default("not-used"),fastUrl:f.string().optional().default(""),fastProxyApiKey:f.string().optional().default(""),apiKey:f.string().optional().default(""),baseURL:f.string().optional().default(""),useEnvVar:f.string().optional().default(""),contextWindow:f.number().optional().default(2e5),costInput:f.number().optional().default(0),costOutput:f.number().optional().default(0),costCacheRead:f.number().optional().default(0),costCacheWrite:f.number().optional().default(0)}),_=[{id:"claude-opus-4-6",name:"Claude Opus",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-sonnet-4-6",name:"Claude Sonnet",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0}],B=f.array(O).default(_),N=f.object({name:f.string(),path:f.string(),description:f.string().default(""),enabled:f.boolean().default(!1)}),X=f.object({name:f.string(),description:f.string(),prompt:f.string(),model:f.enum(["sonnet","opus","haiku","inherit"]).default("inherit"),tools:f.array(f.string()).default(["Read","Write","Edit","Glob","Grep","WebSearch","WebFetch"]),expandContext:f.boolean().default(!1),enabled:f.boolean().default(!1)}),$=f.object({type:f.enum(["claudecode","pi"]).default("claudecode"),piModelRef:f.string().default("")}).passthrough(),V={type:"claudecode",piModelRef:""},H=f.object({enabled:f.boolean().default(!1),modelRefs:f.array(f.string()).default([]),rollingMemoryModel:f.string().default("")}),Q={enabled:!1,modelRefs:[],rollingMemoryModel:""},Z=f.object({maxAttempts:f.number().default(5),baseDelayMs:f.number().default(2e3),maxDelayMs:f.number().default(3e4)}),J={maxAttempts:5,baseDelayMs:2e3,maxDelayMs:3e4},Y=f.object({model:f.string().default("claude-opus-4-6"),mainFallback:f.string().default(""),engine:$.optional().default(V),picoAgent:H.optional().default(Q),maxTurns:f.number().default(50),permissionMode:f.string().default("bypassPermissions"),sessionTTL:f.number().default(3600),queueMode:f.enum(["queue","collect","steer"]).default("steer"),queueDebounceMs:f.number().default(1500),queueCap:f.number().default(20),queueDropPolicy:f.enum(["old","new","summarize"]).default("summarize"),allowedTools:f.array(f.string()).default([]),disallowedTools:f.array(f.string()).default([]),mcpServers:f.record(f.string(),G).default({}),workspacePath:f.string().default("./workspace"),builtinCoderSkill:f.boolean().default(!1),settingSources:f.enum(["nothing","user","project","both"]).default("project"),customSubAgents:f.array(X).default([]),plugins:f.array(N).default([]),inflightTyping:f.boolean().default(!0),autoApproveTools:f.boolean().default(!0),autoRenew:f.number().default(0),apiRetry:Z.optional().default(J),skillNudge:f.object({enabled:f.boolean().default(!0),threshold:f.number().default(10)}).default({enabled:!0,threshold:10}),qualityGate:f.object({enabled:f.boolean().default(!1)}).default({enabled:!1})}),ee={enabled:!1,provider:"openai",maxTextLength:4096,timeoutMs:3e4,edge:{voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"},openai:{modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"},elevenlabs:{modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"}},te={enabled:!0,recallStrategy:"builtin-only",dir:"",search:K,l0:F},ae={model:"claude-opus-4-6",mainFallback:"",engine:V,picoAgent:Q,maxTurns:50,permissionMode:"bypassPermissions",sessionTTL:3600,queueMode:"steer",queueDebounceMs:1500,queueCap:20,queueDropPolicy:"summarize",allowedTools:[],disallowedTools:[],mcpServers:{},workspacePath:"./workspace",builtinCoderSkill:!1,settingSources:"project",customSubAgents:[],plugins:[],inflightTyping:!0,autoApproveTools:!0,autoRenew:0,apiRetry:J,skillNudge:{enabled:!0,threshold:10},qualityGate:{enabled:!1}},oe=f.object({enabled:f.boolean().default(!1),every:f.number().default(18e5),channel:f.string().default(""),chatId:f.string().default(""),message:f.string().default(""),ackMaxChars:f.number().default(300)}),ne={enabled:!1,every:18e5,channel:"",chatId:"",message:"",ackMaxChars:300},le=f.object({enabled:f.boolean().default(!0),isolated:f.boolean().default(!0),broadcastEvents:f.boolean().default(!1),storePath:f.string().default(""),heartbeat:oe.optional().default(ne)}),re={enabled:!0,isolated:!0,broadcastEvents:!1,storePath:"",heartbeat:ne},se=f.object({enabled:f.boolean().default(!0),port:f.number().default(3001),basePath:f.string().default("/nostromo"),configCheckInterval:f.number().default(5),autoRestart:f.boolean().default(!0)}),ie={enabled:!0,port:3001,basePath:"/nostromo",configCheckInterval:5,autoRestart:!0},de=f.object({gmabPath:f.string().optional().default("~/gmab"),host:f.string().optional().default("127.0.0.1"),logLevel:f.enum(["debug","info","warn","error"]).optional().default("info"),verboseDebugLogs:f.boolean().optional().default(!0),timezone:f.string().optional().default(""),fastProxyUrl:f.string().optional().default("http://localhost:4181"),channels:S.optional().default({telegram:{enabled:!1,accounts:{}},whatsapp:{enabled:!1,accounts:{}},discord:{enabled:!1,accounts:{}},slack:{enabled:!1,accounts:{}},signal:{enabled:!1,accounts:{}},msteams:{enabled:!1,accounts:{}},googlechat:{enabled:!1,accounts:{}},line:{enabled:!1,accounts:{}},matrix:{enabled:!1,accounts:{}},mesh:{enabled:!1,brokerUrl:"ws://127.0.0.1:3780/ws",agentId:"",privateKey:"",reconnectDelayMs:5e3},responses:{enabled:!0,port:3004}}),models:B.optional().default(_),stt:D.optional().default({enabled:!1,provider:"openai-whisper","openai-whisper":{modelRef:"",model:"whisper-1",language:""},"local-whisper":{binaryPath:"whisper",model:"base"}}),tts:L.optional().default(ee),memory:q.optional().default(te),agent:Y.optional().default(ae),cron:le.optional().default(re),nostromo:se.optional().default(ie),browser:p.optional().default({enabled:!1,controlPort:3002,headless:!1,noSandbox:!1,attachOnly:!1,remoteCdpTimeoutMs:1500,profiles:{default:{cdpPort:9222,color:"#FF4500"}}})});function ue(e){const t=function(e){const t=new Set,a=/\$\{([^}]+)\}/g;let o;for(;null!==(o=a.exec(e));)t.add(o[1]);return Array.from(t)}(e),a=t.filter(e=>!process.env[e]);a.length>0&&b.warn(`Missing environment variables referenced in config: ${a.join(", ")}. Add them to .env or export them before starting.`)}function ce(e){if("string"==typeof e)return e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(Array.isArray(e))return e.map(ce);if(null!==e&&"object"==typeof e){const t={};for(const[a,o]of Object.entries(e))t[a]=ce(o);return t}return e}const fe={channels:{responses:{enabled:!0,port:3004}},stt:{enabled:!1},tts:ee,memory:te,agent:{...ae,permissionMode:"bypassPermissions",allowedTools:["Read","Grep","Bash","WebSearch","Glob","Write","Edit","WebFetch","Task","Skill"]},cron:re,nostromo:ie};export function loadConfig(n){const l=n??r(process.cwd(),"config.yaml"),i=r(process.cwd(),".env");if(a(i)&&(d({path:i}),b.info(`Loaded .env from ${i}`)),!a(l)){const e="# GrabMeABeer Configuration\n# Configure channels and settings via Nostromo: http://localhost:3001\n\n"+c({gmabPath:"~/gmab",...fe});t(l,e,"utf-8")}const f=e(l,"utf-8");ue(f);const p=ce(u(f)),m=de.parse(p);if(h=process.env.GMAB_PATH?g(process.env.GMAB_PATH):g(m.gmabPath),y=s(h,"data"),o(y,{recursive:!0}),!m.timezone){m.timezone=Intl.DateTimeFormat().resolvedOptions().timeZone;try{const a=u(e(l,"utf-8"))??{};a.timezone=m.timezone,t(l,c(a),"utf-8"),b.info(`Timezone auto-detected and saved: ${m.timezone}`)}catch(e){}}return function(e){o(y,{recursive:!0});const t=process.env.WORKSPACE_PATH??e.agent.workspacePath;e.agent.workspacePath=r(g(t)),o(e.agent.workspacePath,{recursive:!0});const a=s(y,"cron");o(a,{recursive:!0});const n=e.cron.storePath.trim()?r(g(e.cron.storePath)):s(a,"jobs.json");return{...e,gmabPath:h,dataDir:y,dbPath:s(y,"core.db"),memoryDir:s(y,"memory"),cronStorePath:n}}(m)}export function loadRawConfig(t){const o=t??r(process.cwd(),"config.yaml");if(!a(o))return{};const n=e(o,"utf-8");return u(n)??{}}export function resolveModelEntry(e,t){if(!t)return;const a=e.models;if(!a?.length)return;const o=t.indexOf(":");if(o>=0){const e=t.substring(0,o),n=t.substring(o+1);return a.find(t=>t.name===e&&t.id===n)}return a.find(e=>e.name===t)??a.find(e=>e.id===t)}export function modelRefName(e){if(!e)return e;const t=e.indexOf(":");return t>=0?e.substring(0,t):e}export function resolveModelId(e,t){if(!t)return t;const a=resolveModelEntry(e,t);return a?a.id:t}
|
|
1
|
+
import{readFileSync as e,writeFileSync as t,existsSync as a,mkdirSync as o,renameSync as n,unlinkSync as l}from"node:fs";import{resolve as r,join as i}from"node:path";import{homedir as s}from"node:os";import{config as d}from"dotenv";import{parse as u,stringify as c}from"yaml";import{z as f}from"zod";import{BrowserConfigSchema as m}from"@hera-al/browser-server/config";import{createLogger as p}from"./utils/logger.js";const b=p("Config");function g(e){return"~"===e||e.startsWith("~/")?e.replace("~",s()):e}let h=g(process.env.GMAB_PATH??"~/gmab"),y=i(h,"data");export function getGmabPath(){return h}export function getDataDir(){return y}export function getNostromoKeyPath(){return i(y,".nostromo-key")}export function backupConfig(o){if(a(o))try{const r=e=>`${o}.backup${e}`;a(r(1))&&l(r(1));for(let e=2;e<=5;e++)a(r(e))&&n(r(e),r(e-1));t(r(5),e(o)),b.debug(`Config backup created: ${r(5)}`)}catch(e){b.warn(`Failed to create config backup: ${e}`)}}const v=f.record(f.string(),f.any()),x=f.object({reactions:f.boolean().default(!0),sendMessage:f.boolean().default(!0),editMessage:f.boolean().default(!0),deleteMessage:f.boolean().default(!0),sticker:f.boolean().default(!1),createForumTopic:f.boolean().default(!1)}),w=f.object({maxAttempts:f.number().default(3),baseDelayMs:f.number().default(1e3),maxDelayMs:f.number().default(3e4)}),M=f.enum(["off","dm","group","all","allowlist"]),P=f.object({botToken:f.string(),dmPolicy:f.enum(["open","token","allowlist"]).default("allowlist"),allowFrom:f.array(f.union([f.string(),f.number()])).default([]),name:f.string().optional(),reactionLevel:f.enum(["off","ack","minimal","extensive"]).default("ack"),reactionNotifications:f.enum(["off","own","all"]).default("off"),inlineButtonsScope:M.optional(),textChunkLimit:f.number().default(4e3),streamMode:f.enum(["off","partial","block"]).default("partial"),linkPreview:f.boolean().default(!0),actions:x.optional(),retry:w.optional(),timeoutSeconds:f.number().optional(),proxy:f.string().optional(),inputDebounceMs:f.number().default(1500)}),k=f.object({enabled:f.boolean().default(!1),accounts:f.record(f.string(),P).default({})}),j=f.object({enabled:f.boolean().default(!1),accounts:v.default({})}),R=f.object({enabled:f.boolean().default(!0),port:f.number().default(3004)}),C=f.object({maxPerPair:f.number().default(20),windowMs:f.number().default(36e5)}),S=f.object({enabled:f.boolean().default(!1),brokerUrl:f.string().default("ws://127.0.0.1:3780/ws"),agentId:f.string().default(""),privateKey:f.string().default(""),reconnectDelayMs:f.number().default(5e3),rateLimit:C.optional().default({maxPerPair:20,windowMs:36e5})}),T=f.object({telegram:k.optional().default({enabled:!1,accounts:{}}),whatsapp:j.optional().default({enabled:!1,accounts:{}}),discord:j.optional().default({enabled:!1,accounts:{}}),slack:j.optional().default({enabled:!1,accounts:{}}),signal:j.optional().default({enabled:!1,accounts:{}}),msteams:j.optional().default({enabled:!1,accounts:{}}),googlechat:j.optional().default({enabled:!1,accounts:{}}),line:j.optional().default({enabled:!1,accounts:{}}),matrix:j.optional().default({enabled:!1,accounts:{}}),mesh:S.optional().default({enabled:!1,brokerUrl:"ws://127.0.0.1:3780/ws",agentId:"",privateKey:"",reconnectDelayMs:5e3,rateLimit:{maxPerPair:20,windowMs:36e5}}),responses:R.optional().default({enabled:!0,port:3e3})}),A=f.object({modelRef:f.string().default(""),model:f.string().default("whisper-1"),language:f.string().default("")}),D=f.object({binaryPath:f.string().default("whisper"),model:f.string().default("base")}),I=f.object({enabled:f.boolean().default(!1),provider:f.string().default("openai-whisper"),"openai-whisper":A.optional().default({modelRef:"",model:"whisper-1",language:""}),"local-whisper":D.optional().default({binaryPath:"whisper",model:"base"})}),L=f.object({voice:f.string().default("en-US-MichelleNeural"),lang:f.string().default("en-US"),outputFormat:f.string().default("audio-24khz-48kbitrate-mono-mp3")}),U=f.object({modelRef:f.string().default(""),model:f.string().default("gpt-4o-mini-tts"),voice:f.string().default("alloy")}),z=f.object({modelRef:f.string().default(""),voiceId:f.string().default("pMsXgVXv3BLzUgSXRplE"),modelId:f.string().default("eleven_multilingual_v2")}),E=f.object({enabled:f.boolean().default(!1),provider:f.enum(["edge","openai","elevenlabs"]).default("openai"),maxTextLength:f.number().default(4096),timeoutMs:f.number().default(3e4),edge:L.optional().default({voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"}),openai:U.optional().default({modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"}),elevenlabs:z.optional().default({modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"})}),W=f.object({enabled:f.boolean().default(!1),embeddingModel:f.string().default("text-embedding-3-small"),embeddingDimensions:f.number().default(1536),modelRef:f.string().default(""),prefixQuery:f.string().default(""),prefixDocument:f.string().default(""),updateDebounceMs:f.number().default(3e3),embedIntervalMs:f.number().default(3e5),maxResults:f.number().default(6),maxSnippetChars:f.number().default(700),maxInjectedChars:f.number().default(4e3),rrfK:f.number().default(60)}),F={enabled:!1,embeddingModel:"text-embedding-3-small",embeddingDimensions:1536,modelRef:"",prefixQuery:"",prefixDocument:"",updateDebounceMs:3e3,embedIntervalMs:3e5,maxResults:6,maxSnippetChars:700,maxInjectedChars:4e3,rrfK:60},K=f.object({enabled:f.boolean().default(!0),model:f.string().default("")}),_={enabled:!0,model:""},O=f.object({enabled:f.boolean().default(!0),recallStrategy:f.enum(["builtin-only","search"]).default("builtin-only"),search:W.optional().default(F),l0:K.optional().default(_)}),q=f.object({command:f.string(),args:f.array(f.string()).optional(),env:f.record(f.string(),f.string()).optional()}).passthrough(),G=f.object({id:f.string(),name:f.string(),types:f.array(f.enum(["internal","external","env-var"])).optional().default(["external"]),proxy:f.enum(["not-used","direct","proxied"]).optional().default("not-used"),fastUrl:f.string().optional().default(""),fastProxyApiKey:f.string().optional().default(""),apiKey:f.string().optional().default(""),baseURL:f.string().optional().default(""),useEnvVar:f.string().optional().default(""),contextWindow:f.number().optional().default(2e5),costInput:f.number().optional().default(0),costOutput:f.number().optional().default(0),costCacheRead:f.number().optional().default(0),costCacheWrite:f.number().optional().default(0)}),N=[{id:"claude-opus-4-6",name:"Claude Opus",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-sonnet-4-6",name:"Claude Sonnet",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0}],B=f.array(G).default(N),X=f.object({name:f.string(),path:f.string(),description:f.string().default(""),enabled:f.boolean().default(!1)}),$=f.object({name:f.string(),description:f.string(),prompt:f.string(),model:f.enum(["sonnet","opus","haiku","inherit"]).default("inherit"),tools:f.array(f.string()).default(["Read","Write","Edit","Glob","Grep","WebSearch","WebFetch"]),expandContext:f.boolean().default(!1),enabled:f.boolean().default(!1)}),V=f.object({type:f.enum(["claudecode","pi"]).default("claudecode"),piModelRef:f.string().default("")}).passthrough(),H={type:"claudecode",piModelRef:""},Q=f.object({enabled:f.boolean().default(!1),modelRefs:f.array(f.string()).default([]),rollingMemoryModel:f.string().default("")}),Z={enabled:!1,modelRefs:[],rollingMemoryModel:""},J=f.object({maxAttempts:f.number().default(5),baseDelayMs:f.number().default(2e3),maxDelayMs:f.number().default(3e4)}),Y={maxAttempts:5,baseDelayMs:2e3,maxDelayMs:3e4},ee=f.object({model:f.string().default("claude-opus-4-6"),mainFallback:f.string().default(""),engine:V.optional().default(H),picoAgent:Q.optional().default(Z),maxTurns:f.number().default(50),permissionMode:f.string().default("bypassPermissions"),sessionTTL:f.number().default(3600),queueMode:f.enum(["queue","collect","steer"]).default("steer"),queueDebounceMs:f.number().default(1500),queueCap:f.number().default(20),queueDropPolicy:f.enum(["old","new","summarize"]).default("summarize"),allowedTools:f.array(f.string()).default([]),disallowedTools:f.array(f.string()).default([]),mcpServers:f.record(f.string(),q).default({}),workspacePath:f.string().default("./workspace"),builtinCoderSkill:f.boolean().default(!1),settingSources:f.enum(["nothing","user","project","both"]).default("project"),customSubAgents:f.array($).default([]),plugins:f.array(X).default([]),inflightTyping:f.boolean().default(!0),autoApproveTools:f.boolean().default(!0),autoRenew:f.number().default(0),apiRetry:J.optional().default(Y),skillNudge:f.object({enabled:f.boolean().default(!0),threshold:f.number().default(10)}).default({enabled:!0,threshold:10}),qualityGate:f.object({enabled:f.boolean().default(!1)}).default({enabled:!1}),compaction:f.object({enabled:f.boolean().default(!0),contextTokenThreshold:f.number().optional()}).optional()}),te={enabled:!1,provider:"openai",maxTextLength:4096,timeoutMs:3e4,edge:{voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"},openai:{modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"},elevenlabs:{modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"}},ae={enabled:!0,recallStrategy:"builtin-only",dir:"",search:F,l0:_},oe={model:"claude-opus-4-6",mainFallback:"",engine:H,picoAgent:Z,maxTurns:50,permissionMode:"bypassPermissions",sessionTTL:3600,queueMode:"steer",queueDebounceMs:1500,queueCap:20,queueDropPolicy:"summarize",allowedTools:[],disallowedTools:[],mcpServers:{},workspacePath:"./workspace",builtinCoderSkill:!1,settingSources:"project",customSubAgents:[],plugins:[],inflightTyping:!0,autoApproveTools:!0,autoRenew:0,apiRetry:Y,skillNudge:{enabled:!0,threshold:10},qualityGate:{enabled:!1}},ne=f.object({enabled:f.boolean().default(!1),every:f.number().default(18e5),channel:f.string().default(""),chatId:f.string().default(""),message:f.string().default(""),ackMaxChars:f.number().default(300)}),le={enabled:!1,every:18e5,channel:"",chatId:"",message:"",ackMaxChars:300},re=f.object({enabled:f.boolean().default(!0),isolated:f.boolean().default(!0),broadcastEvents:f.boolean().default(!1),trackMemory:f.boolean().default(!1),storePath:f.string().default(""),heartbeat:ne.optional().default(le)}),ie={enabled:!0,isolated:!0,broadcastEvents:!1,trackMemory:!1,storePath:"",heartbeat:le},se=f.object({enabled:f.boolean().default(!0),port:f.number().default(3001),basePath:f.string().default("/nostromo"),configCheckInterval:f.number().default(5),autoRestart:f.boolean().default(!0)}),de={enabled:!0,port:3001,basePath:"/nostromo",configCheckInterval:5,autoRestart:!0},ue=f.object({enabled:f.boolean().default(!1),pm2Name:f.string().default("")}),ce=f.object({l2Prefix:f.string().optional().default("Prima di rispondere al messaggio utente che segue, chiama build_operational_context con le keyword che ritieni rilevanti dal messaggio e dal contesto conversazionale. Poi rispondi considerando il contesto ottenuto.\n---"),applyL2PrefixOnEachUserMessage:f.boolean().optional().default(!0),removeL2PrefixFromMemory:f.boolean().optional().default(!0)}),fe=f.object({gmabPath:f.string().optional().default("~/gmab"),host:f.string().optional().default("127.0.0.1"),logLevel:f.enum(["debug","info","warn","error"]).optional().default("info"),verboseDebugLogs:f.boolean().optional().default(!0),timezone:f.string().optional().default(""),fastProxyUrl:f.string().optional().default("http://localhost:4181"),channels:T.optional().default({telegram:{enabled:!1,accounts:{}},whatsapp:{enabled:!1,accounts:{}},discord:{enabled:!1,accounts:{}},slack:{enabled:!1,accounts:{}},signal:{enabled:!1,accounts:{}},msteams:{enabled:!1,accounts:{}},googlechat:{enabled:!1,accounts:{}},line:{enabled:!1,accounts:{}},matrix:{enabled:!1,accounts:{}},mesh:{enabled:!1,brokerUrl:"ws://127.0.0.1:3780/ws",agentId:"",privateKey:"",reconnectDelayMs:5e3,rateLimit:{maxPerPair:20,windowMs:36e5}},responses:{enabled:!0,port:3004}}),models:B.optional().default(N),stt:I.optional().default({enabled:!1,provider:"openai-whisper","openai-whisper":{modelRef:"",model:"whisper-1",language:""},"local-whisper":{binaryPath:"whisper",model:"base"}}),tts:E.optional().default(te),memory:O.optional().default(ae),agent:ee.optional().default(oe),cron:re.optional().default(ie),nostromo:se.optional().default(de),browser:m.optional().default({enabled:!1,controlPort:3002,headless:!1,noSandbox:!1,attachOnly:!1,remoteCdpTimeoutMs:1500,profiles:{default:{cdpPort:9222,color:"#FF4500"}}}),warmRestart:ue.optional().default({enabled:!1,pm2Name:""}),ckr:ce.optional().default({l2Prefix:"Prima di rispondere al messaggio utente che segue, chiama build_operational_context con le keyword che ritieni rilevanti dal messaggio e dal contesto conversazionale. Poi rispondi considerando il contesto ottenuto.\n---",applyL2PrefixOnEachUserMessage:!0,removeL2PrefixFromMemory:!0})});function me(e){const t=function(e){const t=new Set,a=/\$\{([^}]+)\}/g;let o;for(;null!==(o=a.exec(e));)t.add(o[1]);return Array.from(t)}(e),a=t.filter(e=>!process.env[e]);a.length>0&&b.warn(`Missing environment variables referenced in config: ${a.join(", ")}. Add them to .env or export them before starting.`)}function pe(e){if("string"==typeof e)return e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(Array.isArray(e))return e.map(pe);if(null!==e&&"object"==typeof e){const t={};for(const[a,o]of Object.entries(e))t[a]=pe(o);return t}return e}const be={channels:{responses:{enabled:!0,port:3004}},stt:{enabled:!1},tts:te,memory:ae,agent:{...oe,permissionMode:"bypassPermissions",allowedTools:["Read","Grep","Bash","WebSearch","Glob","Write","Edit","WebFetch","Task","Skill"]},cron:ie,nostromo:de};export function loadConfig(n){const l=n??r(process.cwd(),"config.yaml"),s=r(process.cwd(),".env");if(a(s)&&(d({path:s}),b.info(`Loaded .env from ${s}`)),!a(l)){const e="# GrabMeABeer Configuration\n# Configure channels and settings via Nostromo: http://localhost:3001\n\n"+c({gmabPath:"~/gmab",...be});t(l,e,"utf-8")}const f=e(l,"utf-8");me(f);const m=pe(u(f)),p=fe.parse(m);if(h=process.env.GMAB_PATH?g(process.env.GMAB_PATH):g(p.gmabPath),y=i(h,"data"),o(y,{recursive:!0}),!p.timezone){p.timezone=Intl.DateTimeFormat().resolvedOptions().timeZone;try{const a=u(e(l,"utf-8"))??{};a.timezone=p.timezone,t(l,c(a),"utf-8"),b.info(`Timezone auto-detected and saved: ${p.timezone}`)}catch(e){}}return function(e){o(y,{recursive:!0});const t=process.env.WORKSPACE_PATH??e.agent.workspacePath;e.agent.workspacePath=r(g(t)),o(e.agent.workspacePath,{recursive:!0});const a=i(y,"cron");o(a,{recursive:!0});const n=e.cron.storePath.trim()?r(g(e.cron.storePath)):i(a,"jobs.json");return{...e,gmabPath:h,dataDir:y,dbPath:i(y,"core.db"),memoryDir:i(y,"memory"),cronStorePath:n}}(p)}export function loadRawConfig(t){const o=t??r(process.cwd(),"config.yaml");if(!a(o))return{};const n=e(o,"utf-8");return u(n)??{}}export function resolveModelEntry(e,t){if(!t)return;const a=e.models;if(!a?.length)return;const o=t.indexOf(":");if(o>=0){const e=t.substring(0,o),n=t.substring(o+1);return a.find(t=>t.name===e&&t.id===n)}return a.find(e=>e.name===t)??a.find(e=>e.id===t)}export function modelRefName(e){if(!e)return e;const t=e.indexOf(":");return t>=0?e.substring(0,t):e}export function resolveModelId(e,t){if(!t)return t;const a=resolveModelEntry(e,t);return a?a.id:t}
|
package/dist/gateway/bridge.d.ts
CHANGED
|
@@ -18,6 +18,16 @@ export interface IncomingMessage {
|
|
|
18
18
|
rawContext?: unknown;
|
|
19
19
|
}
|
|
20
20
|
export type MessageHandler = (msg: IncomingMessage) => Promise<string>;
|
|
21
|
+
export interface IncomingReaction {
|
|
22
|
+
chatId: string;
|
|
23
|
+
userId: string;
|
|
24
|
+
channelName: string;
|
|
25
|
+
messageId: number;
|
|
26
|
+
emoji: string;
|
|
27
|
+
removed: boolean;
|
|
28
|
+
username?: string;
|
|
29
|
+
}
|
|
30
|
+
export type ReactionHandler = (reaction: IncomingReaction) => void;
|
|
21
31
|
export type InlineButton = {
|
|
22
32
|
text: string;
|
|
23
33
|
callbackData?: string;
|
|
@@ -25,14 +35,14 @@ export type InlineButton = {
|
|
|
25
35
|
};
|
|
26
36
|
export interface ChannelAdapter {
|
|
27
37
|
readonly name: string;
|
|
28
|
-
start(onMessage: MessageHandler): Promise<void>;
|
|
38
|
+
start(onMessage: MessageHandler, onReaction?: ReactionHandler): Promise<void>;
|
|
29
39
|
sendText(chatId: string, text: string): Promise<void>;
|
|
30
40
|
sendAudio?(chatId: string, filePath: string, asVoice?: boolean): Promise<void>;
|
|
31
41
|
sendButtons?(chatId: string, text: string, buttons: InlineButton[][]): Promise<void>;
|
|
42
|
+
/** One-shot: fire a single platform-native typing signal. */
|
|
32
43
|
setTyping?(chatId: string): Promise<void>;
|
|
44
|
+
/** One-shot: fire a single platform-native stop-typing signal. */
|
|
33
45
|
clearTyping?(chatId: string): Promise<void>;
|
|
34
|
-
/** Cooperative typing release: decrements refcount instead of force-clearing. */
|
|
35
|
-
releaseTyping?(chatId: string): Promise<void>;
|
|
36
46
|
stop(): Promise<void>;
|
|
37
47
|
}
|
|
38
48
|
//# sourceMappingURL=bridge.d.ts.map
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { AppConfig } from "../config.js";
|
|
2
2
|
import type { TokenDB } from "../auth/token-db.js";
|
|
3
|
-
import type { ChannelAdapter, InlineButton, MessageHandler } from "./bridge.js";
|
|
3
|
+
import type { ChannelAdapter, InlineButton, MessageHandler, ReactionHandler } from "./bridge.js";
|
|
4
4
|
export declare class ChannelManager {
|
|
5
5
|
private config;
|
|
6
6
|
private tokenDb;
|
|
7
7
|
private onMessage;
|
|
8
|
+
private onReaction?;
|
|
8
9
|
private adapters;
|
|
9
|
-
constructor(config: AppConfig, tokenDb: TokenDB, onMessage: MessageHandler);
|
|
10
|
+
constructor(config: AppConfig, tokenDb: TokenDB, onMessage: MessageHandler, onReaction?: ReactionHandler | undefined);
|
|
10
11
|
registerAdapter(adapter: ChannelAdapter): void;
|
|
11
12
|
startAll(): Promise<void>;
|
|
12
13
|
sendToChannel(channelName: string, chatId: string, text: string): Promise<void>;
|
|
@@ -14,13 +15,6 @@ export declare class ChannelManager {
|
|
|
14
15
|
sendButtons(channelName: string, chatId: string, text: string, buttons: InlineButton[][]): Promise<void>;
|
|
15
16
|
setTyping(channelName: string, chatId: string): Promise<void>;
|
|
16
17
|
clearTyping(channelName: string, chatId: string): Promise<void>;
|
|
17
|
-
/**
|
|
18
|
-
* Cooperative typing release: decrements the inflight refcount instead of
|
|
19
|
-
* force-clearing. The typing interval will self-destruct when no more
|
|
20
|
-
* handlers are in-flight. Use this in the normal message-completion path;
|
|
21
|
-
* reserve clearTyping() as a safety-net for hard resets.
|
|
22
|
-
*/
|
|
23
|
-
releaseTyping(channelName: string, chatId: string): Promise<void>;
|
|
24
18
|
/**
|
|
25
19
|
* Send a full response that may contain MEDIA: lines.
|
|
26
20
|
* Parses out media entries, sends audio via sendAudio, and sends remaining text via sendText.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{parseMediaLines as t}from"../utils/media-response.js";import{createLogger as e}from"../utils/logger.js";const s=e("ChannelManager");export class ChannelManager{config;tokenDb;onMessage;adapters=new Map;constructor(t,e,s){this.config=t,this.tokenDb=e,this.onMessage=s}registerAdapter(t){this.adapters.set(t.name,t)}async startAll(){const t=[];for(const[e,
|
|
1
|
+
import{parseMediaLines as t}from"../utils/media-response.js";import{createLogger as e}from"../utils/logger.js";const s=e("ChannelManager");export class ChannelManager{config;tokenDb;onMessage;onReaction;adapters=new Map;constructor(t,e,s,n){this.config=t,this.tokenDb=e,this.onMessage=s,this.onReaction=n}registerAdapter(t){this.adapters.set(t.name,t)}async startAll(){const t=[];for(const[e,n]of this.adapters)s.info(`Starting channel: ${e}`),t.push(n.start(this.onMessage,this.onReaction).then(()=>{s.info(`Channel started: ${e}`)}).catch(t=>{s.error(`Failed to start channel ${e}: ${t}`)}));await Promise.allSettled(t)}async sendToChannel(t,e,n){const a=this.adapters.get(t);a?await a.sendText(e,n):s.error(`Channel not found: ${t}`)}async sendAudio(t,e,n,a){const o=this.adapters.get(t);o?o.sendAudio?await o.sendAudio(e,n,a):s.warn(`Channel ${t} does not support audio, skipping`):s.error(`Channel not found: ${t}`)}async sendButtons(t,e,n,a){const o=this.adapters.get(t);if(o)if(o.sendButtons)await o.sendButtons(e,n,a);else{const t=a.flat().map((t,e)=>`${e+1}. ${t.text}`).join("\n");await o.sendText(e,`${n}\n\n${t}\n\nReply with your choice or type your answer.`)}else s.error(`Channel not found: ${t}`)}async setTyping(t,e){const s=this.adapters.get(t);if(s&&s.setTyping)try{await s.setTyping(e)}catch{}}async clearTyping(t,e){const s=this.adapters.get(t);if(s&&s.clearTyping)try{await s.clearTyping(e)}catch{}}async sendResponse(e,n,a){const{textParts:o,mediaEntries:r}=t(a);for(const t of r)try{await this.sendAudio(e,n,t.path,t.asVoice)}catch(t){s.error(`Failed to send audio to ${e}:${n}: ${t}`)}const i=o.join("\n").trim();i&&await this.sendToChannel(e,n,i)}getAdapter(t){return this.adapters.get(t)}getChannel(t){return this.adapters.get(t)}async sendSystemMessage(t,e,n){const a=this.adapters.get(t);if(a)try{await a.sendText(e,n),s.debug(`System message sent to ${t}:${e}`)}catch(n){s.error(`Failed to send system message to ${t}:${e}: ${n}`)}else s.warn(`Cannot send system message: channel ${t} not found`)}listAdapters(){return[...this.adapters.entries()].map(([t])=>({name:t,active:!0}))}async stopAll(){const t=[];for(const[e,n]of this.adapters)s.info(`Stopping channel: ${e}`),t.push(n.stop().catch(t=>{s.error(`Error stopping channel ${e}: ${t}`)}));await Promise.allSettled(t)}}
|
|
@@ -6,11 +6,16 @@
|
|
|
6
6
|
* Dante can reply via sendText("beatrice", "...").
|
|
7
7
|
*/
|
|
8
8
|
import type { ChannelAdapter, MessageHandler } from "../bridge.js";
|
|
9
|
+
export interface MeshRateLimitConfig {
|
|
10
|
+
maxPerPair: number;
|
|
11
|
+
windowMs: number;
|
|
12
|
+
}
|
|
9
13
|
export interface MeshChannelConfig {
|
|
10
14
|
brokerUrl: string;
|
|
11
15
|
agentId: string;
|
|
12
16
|
privateKey: string;
|
|
13
17
|
reconnectDelayMs?: number;
|
|
18
|
+
rateLimit?: MeshRateLimitConfig;
|
|
14
19
|
}
|
|
15
20
|
export declare class MeshChannel implements ChannelAdapter {
|
|
16
21
|
readonly name = "mesh";
|
|
@@ -27,7 +32,16 @@ export declare class MeshChannel implements ChannelAdapter {
|
|
|
27
32
|
private pendingOutbound;
|
|
28
33
|
/** Callback invoked when a reply arrives for an outbound message we sent. */
|
|
29
34
|
private replyCallback;
|
|
35
|
+
/** Track active inbound message per peer — so sendText during processing auto-sets replyTo. */
|
|
36
|
+
private activeInbound;
|
|
37
|
+
private rateBuckets;
|
|
38
|
+
private rateLimitConfig;
|
|
30
39
|
constructor(config: MeshChannelConfig);
|
|
40
|
+
/**
|
|
41
|
+
* Check if sending to a peer is allowed under the rate limit.
|
|
42
|
+
* Returns true if allowed, false if rate-limited.
|
|
43
|
+
*/
|
|
44
|
+
private checkRateLimit;
|
|
31
45
|
/**
|
|
32
46
|
* Register a callback for when a reply arrives for an outbound message.
|
|
33
47
|
* The callback receives the origin session key, the agent ID that replied, and the reply text.
|