@hera-al/server 1.6.4 → 1.6.5

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.
@@ -18,9 +18,10 @@ export declare class AgentService {
18
18
  private ttsToolsServer;
19
19
  private memoryToolsServer;
20
20
  private browserToolsServer;
21
+ private picoToolsServer;
21
22
  private channelManager;
22
23
  private showToolUseGetter;
23
- constructor(config: AppConfig, nodeRegistry?: NodeRegistry, channelManager?: ChannelManager, serverToolsServer?: ReturnType<typeof createServerToolsServer>, cronToolsServer?: unknown, sessionDb?: import("./session-db.js").SessionDB, ttsToolsServer?: ReturnType<typeof createTTSToolsServer>, memoryToolsServer?: unknown, showToolUseGetter?: (sessionKey: string) => boolean, browserToolsServer?: unknown);
24
+ constructor(config: AppConfig, nodeRegistry?: NodeRegistry, channelManager?: ChannelManager, serverToolsServer?: ReturnType<typeof createServerToolsServer>, cronToolsServer?: unknown, sessionDb?: import("./session-db.js").SessionDB, ttsToolsServer?: ReturnType<typeof createTTSToolsServer>, memoryToolsServer?: unknown, showToolUseGetter?: (sessionKey: string) => boolean, browserToolsServer?: unknown, picoToolsServer?: unknown);
24
25
  /**
25
26
  * Send a message to the agent for a given session.
26
27
  * Creates a long-lived SessionAgent on first call; subsequent messages for
@@ -1 +1 @@
1
- import{query as e}from"@anthropic-ai/claude-agent-sdk";import{SessionAgent as s}from"./session-agent.js";import{createNodeToolsServer as t}from"../tools/node-tools.js";import{createMessageToolsServer as o}from"../tools/message-tools.js";import{createLogger as r}from"../utils/logger.js";const n=r("AgentService");export class AgentService{config;agents=new Map;usageBySession=new Map;nodeToolsServer=null;messageToolsServer=null;serverToolsServer=null;cronToolsServer=null;ttsToolsServer=null;memoryToolsServer=null;browserToolsServer=null;channelManager=null;showToolUseGetter=null;constructor(e,s,r,n,i,a,l,g,h,c){this.config=e,r&&(this.channelManager=r),s&&(this.nodeToolsServer=t(s)),r&&a&&(this.messageToolsServer=o(r,()=>this.config,a)),n&&(this.serverToolsServer=n),i&&(this.cronToolsServer=i),l&&(this.ttsToolsServer=l),g&&(this.memoryToolsServer=g),c&&(this.browserToolsServer=c),h&&(this.showToolUseGetter=h)}async sendMessage(e,s,t,o,r,i,a,l,g,h){const c=this.getOrCreateAgent(e,t,o,r,i,a,l,g,h);if(i&&i!==c.getModel()){const t=e=>{const s=this.config.models.find(s=>s.id===e);return!(!s?.proxy||"not-used"===s.proxy)},d=t(c.getModel()),u=t(i);if(d||u){n.info(`[${e}] Proxy config change detected (old=${d}, new=${u}), restarting session`),this.destroySession(e);const t=this.getOrCreateAgent(e,void 0,o,r,i,a,l,g,h);return await t.send(s)}await c.setModel(i)}try{return await c.send(s)}catch(d){const u=d instanceof Error?d.message:String(d);if(u.includes("INVALID_ARGUMENT")||/API Error: 400/.test(u)){n.warn(`[${e}] Transient API 400 error, retrying once: ${u.slice(0,120)}`),this.agents.delete(e),c.close();const d=this.getOrCreateAgent(e,t,o,r,i,a,l,g,h);try{return await d.send(s)}catch(s){n.error(`[${e}] Retry also failed: ${s}`),this.agents.delete(e),d.close();const o=s instanceof Error?s.message:String(s);if(t)return{response:o.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw s}}if(this.agents.delete(e),c.close(),t)return n.warn(`Session agent failed for ${e}: ${d}`),{response:u.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw d}}hasNodeTools(){return null!==this.nodeToolsServer}hasMessageTools(){return null!==this.messageToolsServer}getToolServers(){const e=[];return this.nodeToolsServer&&e.push(this.nodeToolsServer),this.messageToolsServer&&e.push(this.messageToolsServer),this.serverToolsServer&&e.push(this.serverToolsServer),this.cronToolsServer&&e.push(this.cronToolsServer),this.ttsToolsServer&&e.push(this.ttsToolsServer),this.memoryToolsServer&&e.push(this.memoryToolsServer),this.browserToolsServer&&e.push(this.browserToolsServer),e}getOrCreateAgent(e,t,o,r,n,i,a,l,g){const h=this.agents.get(e);if(h&&h.isActive())return h;h&&(h.close(),this.agents.delete(e));const c=new s(e,this.config,o,r,t,n,this.nodeToolsServer??void 0,this.messageToolsServer??void 0,this.serverToolsServer??void 0,this.cronToolsServer??void 0,i,a,l,this.ttsToolsServer??void 0,this.memoryToolsServer??void 0,this.browserToolsServer??void 0,g);if(this.channelManager){const e=this.channelManager;if(c.setChannelSender(async(s,t,o,r)=>{r&&r.length>0?await e.sendButtons(s,t,o,r):await e.sendToChannel(s,t,o)}),this.showToolUseGetter){const s=this.showToolUseGetter;c.setToolUseNotifier(async(t,o,r)=>{if(!s(`${t}:${o}`))return;const n=`⚙️ Using ${r.replace(/^mcp__[^_]+__/,"")}`;await e.sendToChannel(t,o,n),await e.setTyping(t,o)})}c.setTypingSetter(async(s,t)=>{await e.setTyping(s,t)}),c.setTypingClearer(async(s,t)=>{await e.clearTyping(s,t)}),c.setTextBlockStreamer(async(s,t,o)=>{await e.sendResponse(s,t,o)})}return c.setUsageRecorder((e,s,t,o,r)=>{this.usageBySession.set(e,{totalCostUsd:s,durationMs:t,numTurns:o,modelUsage:r,recordedAt:Date.now()})}),this.agents.set(e,c),c}async interrupt(e){const s=this.agents.get(e);return!!s&&s.interrupt()}isBusy(e){const s=this.agents.get(e);return!!s&&s.isBusy()}hasPendingPermission(e){const s=this.agents.get(e);return!!s&&s.hasPendingPermission()}resolvePermission(e,s){const t=this.agents.get(e);t&&t.resolvePermission(s)}hasPendingQuestion(e){const s=this.agents.get(e);return!!s&&s.hasPendingQuestion()}resolveQuestion(e,s){const t=this.agents.get(e);t&&t.resolveQuestion(s)}destroySession(e){const s=this.agents.get(e);s&&(s.close(),this.agents.delete(e),n.info(`Session agent destroyed: ${e}`))}destroyAll(){for(const[e,s]of this.agents)s.close(),n.info(`Session agent destroyed (reconfigure): ${e}`);this.agents.clear()}getActiveSessions(){return Array.from(this.agents.keys()).filter(e=>{const s=this.agents.get(e);return s&&s.isActive()})}getActiveSessionCount(){return this.getActiveSessions().length}getUsage(e){return this.usageBySession.get(e)}getSdkSlashCommands(){for(const e of this.agents.values()){const s=e.getSdkSlashCommands();if(s.length>0)return s}return[]}async listModels(){try{const s=e({prompt:"list models",options:{maxTurns:0}}),t=await s.supportedModels();for await(const e of s)break;return t.map(e=>({id:e.id??e.name??String(e),name:e.name??e.id??String(e)}))}catch(e){return n.error(`Failed to list models: ${e}`),[{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"}]}}}
1
+ import{query as e}from"@anthropic-ai/claude-agent-sdk";import{SessionAgent as s}from"./session-agent.js";import{createNodeToolsServer as t}from"../tools/node-tools.js";import{createMessageToolsServer as o}from"../tools/message-tools.js";import{createLogger as r}from"../utils/logger.js";const n=r("AgentService");export class AgentService{config;agents=new Map;usageBySession=new Map;nodeToolsServer=null;messageToolsServer=null;serverToolsServer=null;cronToolsServer=null;ttsToolsServer=null;memoryToolsServer=null;browserToolsServer=null;picoToolsServer=null;channelManager=null;showToolUseGetter=null;constructor(e,s,r,n,i,a,l,h,g,c,d){this.config=e,r&&(this.channelManager=r),s&&(this.nodeToolsServer=t(s)),r&&a&&(this.messageToolsServer=o(r,()=>this.config,a)),n&&(this.serverToolsServer=n),i&&(this.cronToolsServer=i),l&&(this.ttsToolsServer=l),h&&(this.memoryToolsServer=h),c&&(this.browserToolsServer=c),d&&(this.picoToolsServer=d),g&&(this.showToolUseGetter=g)}async sendMessage(e,s,t,o,r,i,a,l,h,g){const c=this.getOrCreateAgent(e,t,o,r,i,a,l,h,g);if(i&&i!==c.getModel()){const t=e=>{const s=this.config.models.find(s=>s.id===e);return!(!s?.proxy||"not-used"===s.proxy)},d=t(c.getModel()),u=t(i);if(d||u){n.info(`[${e}] Proxy config change detected (old=${d}, new=${u}), restarting session`),this.destroySession(e);const t=this.getOrCreateAgent(e,void 0,o,r,i,a,l,h,g);return await t.send(s)}await c.setModel(i)}try{return await c.send(s)}catch(d){const u=d instanceof Error?d.message:String(d);if(u.includes("INVALID_ARGUMENT")||/API Error: 400/.test(u)){n.warn(`[${e}] Transient API 400 error, retrying once: ${u.slice(0,120)}`),this.agents.delete(e),c.close();const d=this.getOrCreateAgent(e,t,o,r,i,a,l,h,g);try{return await d.send(s)}catch(s){n.error(`[${e}] Retry also failed: ${s}`),this.agents.delete(e),d.close();const o=s instanceof Error?s.message:String(s);if(t)return{response:o.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw s}}if(this.agents.delete(e),c.close(),t)return n.warn(`Session agent failed for ${e}: ${d}`),{response:u.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw d}}hasNodeTools(){return null!==this.nodeToolsServer}hasMessageTools(){return null!==this.messageToolsServer}getToolServers(){const e=[];return this.nodeToolsServer&&e.push(this.nodeToolsServer),this.messageToolsServer&&e.push(this.messageToolsServer),this.serverToolsServer&&e.push(this.serverToolsServer),this.cronToolsServer&&e.push(this.cronToolsServer),this.ttsToolsServer&&e.push(this.ttsToolsServer),this.memoryToolsServer&&e.push(this.memoryToolsServer),this.browserToolsServer&&e.push(this.browserToolsServer),this.picoToolsServer&&e.push(this.picoToolsServer),e}getOrCreateAgent(e,t,o,r,n,i,a,l,h){const g=this.agents.get(e);if(g&&g.isActive())return g;g&&(g.close(),this.agents.delete(e));const c=new s(e,this.config,o,r,t,n,this.nodeToolsServer??void 0,this.messageToolsServer??void 0,this.serverToolsServer??void 0,this.cronToolsServer??void 0,i,a,l,this.ttsToolsServer??void 0,this.memoryToolsServer??void 0,this.browserToolsServer??void 0,h,this.picoToolsServer??void 0);if(this.channelManager){const e=this.channelManager;if(c.setChannelSender(async(s,t,o,r)=>{r&&r.length>0?await e.sendButtons(s,t,o,r):await e.sendToChannel(s,t,o)}),this.showToolUseGetter){const s=this.showToolUseGetter;c.setToolUseNotifier(async(t,o,r)=>{if(!s(`${t}:${o}`))return;const n=`⚙️ Using ${r.replace(/^mcp__[^_]+__/,"")}`;await e.sendToChannel(t,o,n),await e.setTyping(t,o)})}c.setTypingSetter(async(s,t)=>{await e.setTyping(s,t)}),c.setTypingClearer(async(s,t)=>{await e.clearTyping(s,t)}),c.setTextBlockStreamer(async(s,t,o)=>{await e.sendResponse(s,t,o)})}return c.setUsageRecorder((e,s,t,o,r)=>{this.usageBySession.set(e,{totalCostUsd:s,durationMs:t,numTurns:o,modelUsage:r,recordedAt:Date.now()})}),this.agents.set(e,c),c}async interrupt(e){const s=this.agents.get(e);return!!s&&s.interrupt()}isBusy(e){const s=this.agents.get(e);return!!s&&s.isBusy()}hasPendingPermission(e){const s=this.agents.get(e);return!!s&&s.hasPendingPermission()}resolvePermission(e,s){const t=this.agents.get(e);t&&t.resolvePermission(s)}hasPendingQuestion(e){const s=this.agents.get(e);return!!s&&s.hasPendingQuestion()}resolveQuestion(e,s){const t=this.agents.get(e);t&&t.resolveQuestion(s)}destroySession(e){const s=this.agents.get(e);s&&(s.close(),this.agents.delete(e),n.info(`Session agent destroyed: ${e}`))}destroyAll(){for(const[e,s]of this.agents)s.close(),n.info(`Session agent destroyed (reconfigure): ${e}`);this.agents.clear()}getActiveSessions(){return Array.from(this.agents.keys()).filter(e=>{const s=this.agents.get(e);return s&&s.isActive()})}getActiveSessionCount(){return this.getActiveSessions().length}getUsage(e){return this.usageBySession.get(e)}getSdkSlashCommands(){for(const e of this.agents.values()){const s=e.getSdkSlashCommands();if(s.length>0)return s}return[]}async listModels(){try{const s=e({prompt:"list models",options:{maxTurns:0}}),t=await s.supportedModels();for await(const e of s)break;return t.map(e=>({id:e.id??e.name??String(e),name:e.name??e.id??String(e)}))}catch(e){return n.error(`Failed to list models: ${e}`),[{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"}]}}}
@@ -99,7 +99,7 @@ export declare class SessionAgent {
99
99
  private autoApproveTools;
100
100
  private pendingPermission;
101
101
  private pendingQuestion;
102
- 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);
102
+ 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);
103
103
  /**
104
104
  * Resolve Pi provider configuration based on the active model.
105
105
  *
@@ -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}from"../config.js";import{createLogger as o}from"../utils/logger.js";const i=o("SessionAgent");export class SessionAgent{sessionKey;config;queue;queryHandle=null;pendingResponses=[];currentResponse="";currentSessionId;model;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;pendingPermission=null;pendingQuestion=null;constructor(e,o,n,r,l,a,h,u,d,c,p,g,f,m,y,$,b){this.sessionKey=e,this.config=o,this.currentSessionId=l??"";const v=a??o.agent.model;this.model=v?t(o,v):"",this.queueMode=o.agent.queueMode,this.debounceMs=Math.max(0,o.agent.queueDebounceMs),this.queueCap=Math.max(0,o.agent.queueCap),this.dropPolicy=o.agent.queueDropPolicy,this.autoApproveTools=o.agent.autoApproveTools,this.queue=new s,this.opts={...this.model?{model:this.model}:{},systemPrompt:p?{type:"preset",preset:"claude_code",append:n}:n,...o.agent.maxTurns>0?{maxTurns:o.agent.maxTurns}:{},cwd:o.agent.workspacePath,env:process.env,permissionMode:o.agent.permissionMode,allowDangerouslySkipPermissions:!1,...b?{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(i.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),o=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,o,e).catch(()=>{})}}}return{}}]}]},stderr:s=>{i.error(`[${e}] SDK stderr: ${s.trimEnd()}`)}};const T=o.agent.settingSources;"user"===T?this.opts.settingSources=["user"]:"project"===T?this.opts.settingSources=["project"]:"both"===T&&(this.opts.settingSources=["user","project"]);const w=o.agent.mainFallback;w&&(this.opts.fallbackModel=t(o,w)),o.agent.allowedTools.length>0&&(this.opts.allowedTools=o.agent.allowedTools),o.agent.disallowedTools.length>0&&(this.opts.disallowedTools=o.agent.disallowedTools);const S={};if(Object.keys(o.agent.mcpServers).length>0&&Object.assign(S,o.agent.mcpServers),h&&(S["node-tools"]=h),u&&(S["message-tools"]=u),d&&(S["server-tools"]=d),c&&(S["cron-tools"]=c),m&&(S["tts-tools"]=m),y&&(S["memory-tools"]=y),$&&(S["browser-tools"]=$),Object.keys(S).length>0&&(this.opts.mcpServers=S,this.opts.allowedTools&&this.opts.allowedTools.length>0))for(const e of Object.keys(S)){const s=`mcp__${e}__*`;this.opts.allowedTools.includes(s)||this.opts.allowedTools.push(s)}if(l&&(this.opts.resume=l),!1===g&&(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"))),f){const e={};for(const s of o.agent.customSubAgents){if(!s.enabled)continue;const t=s.expandContext?r+"\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 x=o.agent.plugins.filter(e=>e.enabled);x.length>0&&(this.opts.options={...this.opts.options,plugins:x.map(e=>({type:"local",path:e.path}))});const R=this.buildEnvForModel(this.model);this.opts.env=R.env,R.disableThinking&&(this.opts.maxThinkingTokens=0),this.piProviderConfig=this.resolvePiConfig(),this.piProviderConfig&&i.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 o;const n=this.config.agent.picoAgent;if(n?.enabled&&Array.isArray(n.modelRefs)&&(o=n.modelRefs.find(e=>e.split(":")[0]===t)),!o){const e=this.config.agent.engine;if(!e||"pi"!==e.type||!e.piModelRef)return null;o=e.piModelRef}const r=o.split(":");if(r.length<2)return i.warn(`[${this.currentSessionId}] Invalid piModelRef (missing ':'): ${o}`),null;const l=r[0].trim();let a,h;if(r.length>=3)a=r[1].trim(),h=r.slice(2).join(":").trim(),h.startsWith(a+":")&&(h=h.substring(a.length+1));else{const e=r[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);return u?.baseURL&&u.baseURL.includes("openrouter.ai")&&"openrouter"!==a&&(i.info(`[${this.currentSessionId}] piModelRef auto-correction: baseURL is openrouter.ai, switching provider from "${a}" to "openrouter" (modelId: "${a}/${h}")`),h=`${a}/${h}`,a="openrouter"),i.info(`[${this.currentSessionId}] piModelRef resolved: provider="${a}", modelId="${h}", contextWindow=${u?.contextWindow??128e3}`),{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}}async send(e){if(this.closed||this.outputDone)throw new Error("SessionAgent is closed");switch(this.ensureInitialized(),this.queueMode){case"collect":return this.sendCollect(e);case"steer":return this.sendSteer(e);default:return this.sendDirect(e)}}async interrupt(){if(this.closed||!this.queryHandle)return!1;try{return await this.queryHandle.interrupt(),i.info(`[${this.sessionKey}] Interrupted`),!0}catch{return!1}}async setModel(e){if(this.queryHandle)try{await this.queryHandle.setModel(e),this.model=e,i.info(`[${this.sessionKey}] Model changed to ${e}`)}catch(e){i.error(`[${this.sessionKey}] Failed to set model: ${e}`)}}close(){if(this.closed)return;this.closed=!0,this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.debounceResolve&&(this.debounceResolve(),this.debounceResolve=null),this.queue.close(),this.queryHandle&&this.queryHandle.close();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=[],i.info(`[${this.sessionKey}] Closed`)}isActive(){return!this.closed&&!this.outputDone}getSessionId(){return this.currentSessionId}getModel(){return this.model}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="",i.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,i.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?(i.info(`[${this.sessionKey}] Permission approved: ${s.toolName}`),s.resolve({behavior:"allow",updatedInput:s.input})):(i.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,i.info(`[${this.sessionKey}] Question answered: "${e}" for "${s.questionText}"`),s.resolve(e)}async handleCanUseTool(e,s){if("AskUserQuestion"===e){if(!this.channelSender)return i.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),o=this.sessionKey.substring(e+1);if(!t||!o||"cron"===t)return{behavior:"allow",updatedInput:s};const n=s?.questions;if(!Array.isArray(n)||0===n.length)return{behavior:"allow",updatedInput:s};const r={};for(const e of n){const n=e.question||"?",l=Array.isArray(e.options)?e.options:[],a=[];if(e.header&&a.push(`*${e.header}*`),a.push(n),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,o)}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,o,h,e)}else await this.channelSender(t,o,h)}catch(e){return i.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";i.warn(`[${this.sessionKey}] Question timeout, defaulting to "${s}"`),this.channelSender&&this.channelSender(t,o,`[Timeout] Auto-selected: ${s}`).catch(()=>{}),e(s)}},u);this.pendingQuestion={resolve:t=>{clearTimeout(s),e(t)},questionText:n}});if(r[n]=d,this.typingSetter)try{await this.typingSetter(t,o)}catch{}}return i.info(`[${this.sessionKey}] AskUserQuestion answered: ${JSON.stringify(r)}`),{behavior:"allow",updatedInput:{questions:s.questions,answers:r}}}if(this.autoApproveTools)return i.debug(`[${this.sessionKey}] Auto-approving tool: ${e}`),{behavior:"allow",updatedInput:s};if(!this.channelSender)return i.warn(`[${this.sessionKey}] No channel sender for interactive permission, auto-approving: ${e}`),{behavior:"allow",updatedInput:s};const t=this.sessionKey.indexOf(":");if(t<0)return{behavior:"allow",updatedInput:s};const o=this.sessionKey.substring(0,t),n=this.sessionKey.substring(t+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 l=r.join("\n"),a=[{text:"Approve",callbackData:"__tool_perm:approve"},{text:"Deny",callbackData:"__tool_perm:deny"}];try{await this.channelSender(o,n,l,a)}catch(e){return i.error(`[${this.sessionKey}] Failed to send permission request: ${e}`),{behavior:"allow",updatedInput:s}}if(this.typingClearer)try{await this.typingClearer(o,n)}catch{}const h=12e4;return new Promise(t=>{const r=setTimeout(()=>{this.pendingPermission?.resolve===t&&(this.pendingPermission=null,i.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"}))},h);this.pendingPermission={resolve:t,toolName:e,input:s};const l=t;this.pendingPermission.resolve=e=>{clearTimeout(r),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),o=this.sessionKey.substring(s+1);if(!t||!o||"cron"===t)return;const n=e?.questions;if(Array.isArray(n)){for(const e of n){const s=e.question||"?",n=Array.isArray(e.options)?e.options:[],r=[];if(e.header&&r.push(`*${e.header}*`),r.push(s),n.some(e=>e.description)){r.push("");for(const e of n){const s=e.description?`: ${e.description}`:"";r.push(`• ${e.label}${s}`)}}const l=r.join("\n");try{if(n.length>0){const e=n.map(e=>({text:e.label||String(e),callbackData:e.label||String(e)}));await this.channelSender(t,o,l,e)}else await this.channelSender(t,o,l)}catch(e){i.error(`[${this.sessionKey}] Failed to forward AskUserQuestion: ${e}`)}}if(this.typingClearer)try{await this.typingClearer(t,o)}catch(e){i.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),o=this.sessionKey.substring(s+1);if(t&&o&&"cron"!==t)try{await this.toolUseNotifier(t,o,e)}catch(e){i.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 o=this.pendingTextBlock;this.pendingTextBlock="",this.streamedAny=!0,this.streamedText+=o;try{await this.textBlockStreamer(s,t,o)}catch(e){i.error(`[${this.sessionKey}] Text block stream error: ${e}`)}}sendDirect(e){if(this.queueCap>0&&this.pendingResponses.length>=this.queueCap)return i.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),i.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(),i.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&&(i.info(`[${this.sessionKey}] Steer: interrupting current processing`),await this.interrupt()),this.sendDirect(e)}applyDropPolicy(e){if("new"===this.dropPolicy)return i.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}),i.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),o=this.buildQueueMessage(t),n=[...this.droppedResolvers.splice(0),...e.map(e=>({resolve:e.resolve,reject:e.reject}))];this.droppedSummaries=[],this.pendingResponses.push({resolve:e=>{for(const s of n)s.resolve(e)},reject:e=>{for(const s of n)s.reject(e)}}),this.queue.push(o),i.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 o=0;o<e.length;o++)e[o].text&&s.push(`${o+1}. ${e[o].text}`),t.push(...e[o].images)}return{text:s.join("\n"),images:t}}ensureInitialized(){if(this.initialized)return;this.initialized=!0;const s=this.piProviderConfig?"pi":"claudecode";i.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(),i.info(`[${this.sessionKey}] Pi engine initialized: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}catch(s){i.error(`[${this.sessionKey}] Failed to initialize Pi engine: ${s}`),i.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 i.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}),i.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(i.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(" "),i.info(`[${this.sessionKey}] Compact: ${this.currentResponse}`)}else if("init"!==t&&"status"!==t){const{type:e,...o}=s;this.currentResponse=JSON.stringify(o,null,2),i.info(`[${this.sessionKey}] System message (${t??"unknown"}): ${this.currentResponse.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 o=s.map(e=>e.type).join(", ");i.debug(`[${this.sessionKey}] SDK assistant message: blocks=[${o}], text length=${t.length}: ${this.config.verboseDebugLogs?t:t.slice(0,15)+"..."}`);s.some(e=>"tool_use"===e.type)&&this.pendingTextBlock&&this.textBlockStreamer&&await this.flushPendingTextBlock();for(const e of s)if("tool_use"===e.type){const s=JSON.stringify(e.input);i.debug(`[${this.sessionKey}] Tool call: ${e.name} ${this.config.verboseDebugLogs?s:s.slice(0,100)+(s.length>100?"...":"")}`),this.toolUseNotifier&&"AskUserQuestion"!==e.name&&this.notifyToolUse(e.name).catch(e=>{i.error(`[${this.sessionKey}] Tool use notification error: ${e}`)})}}if("tool_progress"===e.type){const s=e;i.debug(`[${this.sessionKey}] Tool progress: ${s.tool_name} (${s.elapsed_time_seconds}s)`)}if("result"===e.type){const s=e;let t;i.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 o=s.stop_reason??null;if("success"===s.subtype){if(s.result)this.currentResponse=s.result;else if(!this.currentResponse&&(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,o]of Object.entries(s.modelUsage)){const s=o,i=[` ${t}:`];s.inputTokens&&i.push(`input=${s.inputTokens}`),s.outputTokens&&i.push(`output=${s.outputTokens}`),s.cacheReadInputTokens&&i.push(`cache_read=${s.cacheReadInputTokens}`),s.cacheCreationInputTokens&&i.push(`cache_create=${s.cacheCreationInputTokens}`),void 0!==s.costUSD&&i.push(`cost=$${Number(s.costUSD).toFixed(4)}`),e.push(i.join(" "))}e.length>0&&(this.currentResponse=e.join("\n"))}if(!s.result&&!this.currentResponse){const e=this.piProviderConfig;i.warn(`[${this.sessionKey}] Empty response on success: provider=${e?.provider??"sdk"}, modelId=${e?.modelId??"n/a"}, stop_reason=${o}. Check provider routing and API key.`)}"refusal"===o?(i.warn(`[${this.sessionKey}] Model refused the request`),this.currentResponse||(this.currentResponse="I'm unable to fulfill this request.")):"max_tokens"===o&&i.warn(`[${this.sessionKey}] Response truncated: output token limit reached`)}else if("error_max_turns"===s.subtype)t="max_turns",i.warn(`[${this.sessionKey}] Max turns reached`);else if("error_max_budget_usd"===s.subtype)t="max_budget",i.warn(`[${this.sessionKey}] Max budget reached`);else{const e=s.errors??[];e.some(e=>e.includes("aborted"))?i.info(`[${this.sessionKey}] Request aborted (steer interrupt)`):i.error(`[${this.sessionKey}] SDK error: ${JSON.stringify(s)}`)}const n=this.pendingResponses.shift();if(n){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)),i.info(`[${this.sessionKey}] Response ready: session=${this.currentSessionId}, length=${s.length}${this.streamedAny?` (streamed, full=${e.length})`:""}`),n.resolve({response:s,fullResponse:this.streamedAny?e:void 0,sessionId:this.currentSessionId,sessionReset:!1,errorType:t,stopReason:o})}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){i.error(`[${this.sessionKey}] Output stream error: ${e}`);const s=this.pendingResponses.shift();s&&(this.currentSessionId?(i.warn(`[${this.sessionKey}] Session corrupted: ${this.currentSessionId}`),s.resolve({response:"",sessionId:"",sessionReset:!0})):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}from"../config.js";import{createLogger as o}from"../utils/logger.js";const i=o("SessionAgent");export class SessionAgent{sessionKey;config;queue;queryHandle=null;pendingResponses=[];currentResponse="";currentSessionId;model;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;pendingPermission=null;pendingQuestion=null;constructor(e,o,n,r,l,a,h,u,d,c,p,g,f,m,y,$,b,v){this.sessionKey=e,this.config=o,this.currentSessionId=l??"";const T=a??o.agent.model;this.model=T?t(o,T):"",this.queueMode=o.agent.queueMode,this.debounceMs=Math.max(0,o.agent.queueDebounceMs),this.queueCap=Math.max(0,o.agent.queueCap),this.dropPolicy=o.agent.queueDropPolicy,this.autoApproveTools=o.agent.autoApproveTools,this.queue=new s,this.opts={...this.model?{model:this.model}:{},systemPrompt:p?{type:"preset",preset:"claude_code",append:n}:n,...o.agent.maxTurns>0?{maxTurns:o.agent.maxTurns}:{},cwd:o.agent.workspacePath,env:process.env,permissionMode:o.agent.permissionMode,allowDangerouslySkipPermissions:!1,...b?{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(i.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),o=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,o,e).catch(()=>{})}}}return{}}]}]},stderr:s=>{i.error(`[${e}] SDK stderr: ${s.trimEnd()}`)}};const w=o.agent.settingSources;"user"===w?this.opts.settingSources=["user"]:"project"===w?this.opts.settingSources=["project"]:"both"===w&&(this.opts.settingSources=["user","project"]);const S=o.agent.mainFallback;S&&(this.opts.fallbackModel=t(o,S)),o.agent.allowedTools.length>0&&(this.opts.allowedTools=o.agent.allowedTools),o.agent.disallowedTools.length>0&&(this.opts.disallowedTools=o.agent.disallowedTools);const x={};if(Object.keys(o.agent.mcpServers).length>0&&Object.assign(x,o.agent.mcpServers),h&&(x["node-tools"]=h),u&&(x["message-tools"]=u),d&&(x["server-tools"]=d),c&&(x["cron-tools"]=c),m&&(x["tts-tools"]=m),y&&(x["memory-tools"]=y),$&&(x["browser-tools"]=$),v&&(x["pico-tools"]=v),Object.keys(x).length>0&&(this.opts.mcpServers=x,this.opts.allowedTools&&this.opts.allowedTools.length>0))for(const e of Object.keys(x)){const s=`mcp__${e}__*`;this.opts.allowedTools.includes(s)||this.opts.allowedTools.push(s)}if(l&&(this.opts.resume=l),!1===g&&(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"))),f){const e={};for(const s of o.agent.customSubAgents){if(!s.enabled)continue;const t=s.expandContext?r+"\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 R=o.agent.plugins.filter(e=>e.enabled);R.length>0&&(this.opts.options={...this.opts.options,plugins:R.map(e=>({type:"local",path:e.path}))});const K=this.buildEnvForModel(this.model);this.opts.env=K.env,K.disableThinking&&(this.opts.maxThinkingTokens=0),this.piProviderConfig=this.resolvePiConfig(),this.piProviderConfig&&i.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 o;const n=this.config.agent.picoAgent;if(n?.enabled&&Array.isArray(n.modelRefs)&&(o=n.modelRefs.find(e=>e.split(":")[0]===t)),!o){const e=this.config.agent.engine;if(!e||"pi"!==e.type||!e.piModelRef)return null;o=e.piModelRef}const r=o.split(":");if(r.length<2)return i.warn(`[${this.currentSessionId}] Invalid piModelRef (missing ':'): ${o}`),null;const l=r[0].trim();let a,h;if(r.length>=3)a=r[1].trim(),h=r.slice(2).join(":").trim(),h.startsWith(a+":")&&(h=h.substring(a.length+1));else{const e=r[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);return u?.baseURL&&u.baseURL.includes("openrouter.ai")&&"openrouter"!==a&&(i.info(`[${this.currentSessionId}] piModelRef auto-correction: baseURL is openrouter.ai, switching provider from "${a}" to "openrouter" (modelId: "${a}/${h}")`),h=`${a}/${h}`,a="openrouter"),i.info(`[${this.currentSessionId}] piModelRef resolved: provider="${a}", modelId="${h}", contextWindow=${u?.contextWindow??128e3}`),{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}}async send(e){if(this.closed||this.outputDone)throw new Error("SessionAgent is closed");switch(this.ensureInitialized(),this.queueMode){case"collect":return this.sendCollect(e);case"steer":return this.sendSteer(e);default:return this.sendDirect(e)}}async interrupt(){if(this.closed||!this.queryHandle)return!1;try{return await this.queryHandle.interrupt(),i.info(`[${this.sessionKey}] Interrupted`),!0}catch{return!1}}async setModel(e){if(this.queryHandle)try{await this.queryHandle.setModel(e),this.model=e,i.info(`[${this.sessionKey}] Model changed to ${e}`)}catch(e){i.error(`[${this.sessionKey}] Failed to set model: ${e}`)}}close(){if(this.closed)return;this.closed=!0,this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.debounceResolve&&(this.debounceResolve(),this.debounceResolve=null),this.queue.close(),this.queryHandle&&this.queryHandle.close();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=[],i.info(`[${this.sessionKey}] Closed`)}isActive(){return!this.closed&&!this.outputDone}getSessionId(){return this.currentSessionId}getModel(){return this.model}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="",i.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,i.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?(i.info(`[${this.sessionKey}] Permission approved: ${s.toolName}`),s.resolve({behavior:"allow",updatedInput:s.input})):(i.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,i.info(`[${this.sessionKey}] Question answered: "${e}" for "${s.questionText}"`),s.resolve(e)}async handleCanUseTool(e,s){if("AskUserQuestion"===e){if(!this.channelSender)return i.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),o=this.sessionKey.substring(e+1);if(!t||!o||"cron"===t)return{behavior:"allow",updatedInput:s};const n=s?.questions;if(!Array.isArray(n)||0===n.length)return{behavior:"allow",updatedInput:s};const r={};for(const e of n){const n=e.question||"?",l=Array.isArray(e.options)?e.options:[],a=[];if(e.header&&a.push(`*${e.header}*`),a.push(n),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,o)}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,o,h,e)}else await this.channelSender(t,o,h)}catch(e){return i.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";i.warn(`[${this.sessionKey}] Question timeout, defaulting to "${s}"`),this.channelSender&&this.channelSender(t,o,`[Timeout] Auto-selected: ${s}`).catch(()=>{}),e(s)}},u);this.pendingQuestion={resolve:t=>{clearTimeout(s),e(t)},questionText:n}});if(r[n]=d,this.typingSetter)try{await this.typingSetter(t,o)}catch{}}return i.info(`[${this.sessionKey}] AskUserQuestion answered: ${JSON.stringify(r)}`),{behavior:"allow",updatedInput:{questions:s.questions,answers:r}}}if(this.autoApproveTools)return i.debug(`[${this.sessionKey}] Auto-approving tool: ${e}`),{behavior:"allow",updatedInput:s};if(!this.channelSender)return i.warn(`[${this.sessionKey}] No channel sender for interactive permission, auto-approving: ${e}`),{behavior:"allow",updatedInput:s};const t=this.sessionKey.indexOf(":");if(t<0)return{behavior:"allow",updatedInput:s};const o=this.sessionKey.substring(0,t),n=this.sessionKey.substring(t+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 l=r.join("\n"),a=[{text:"Approve",callbackData:"__tool_perm:approve"},{text:"Deny",callbackData:"__tool_perm:deny"}];try{await this.channelSender(o,n,l,a)}catch(e){return i.error(`[${this.sessionKey}] Failed to send permission request: ${e}`),{behavior:"allow",updatedInput:s}}if(this.typingClearer)try{await this.typingClearer(o,n)}catch{}const h=12e4;return new Promise(t=>{const r=setTimeout(()=>{this.pendingPermission?.resolve===t&&(this.pendingPermission=null,i.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"}))},h);this.pendingPermission={resolve:t,toolName:e,input:s};const l=t;this.pendingPermission.resolve=e=>{clearTimeout(r),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),o=this.sessionKey.substring(s+1);if(!t||!o||"cron"===t)return;const n=e?.questions;if(Array.isArray(n)){for(const e of n){const s=e.question||"?",n=Array.isArray(e.options)?e.options:[],r=[];if(e.header&&r.push(`*${e.header}*`),r.push(s),n.some(e=>e.description)){r.push("");for(const e of n){const s=e.description?`: ${e.description}`:"";r.push(`• ${e.label}${s}`)}}const l=r.join("\n");try{if(n.length>0){const e=n.map(e=>({text:e.label||String(e),callbackData:e.label||String(e)}));await this.channelSender(t,o,l,e)}else await this.channelSender(t,o,l)}catch(e){i.error(`[${this.sessionKey}] Failed to forward AskUserQuestion: ${e}`)}}if(this.typingClearer)try{await this.typingClearer(t,o)}catch(e){i.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),o=this.sessionKey.substring(s+1);if(t&&o&&"cron"!==t)try{await this.toolUseNotifier(t,o,e)}catch(e){i.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 o=this.pendingTextBlock;this.pendingTextBlock="",this.streamedAny=!0,this.streamedText+=o;try{await this.textBlockStreamer(s,t,o)}catch(e){i.error(`[${this.sessionKey}] Text block stream error: ${e}`)}}sendDirect(e){if(this.queueCap>0&&this.pendingResponses.length>=this.queueCap)return i.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),i.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(),i.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&&(i.info(`[${this.sessionKey}] Steer: interrupting current processing`),await this.interrupt()),this.sendDirect(e)}applyDropPolicy(e){if("new"===this.dropPolicy)return i.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}),i.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),o=this.buildQueueMessage(t),n=[...this.droppedResolvers.splice(0),...e.map(e=>({resolve:e.resolve,reject:e.reject}))];this.droppedSummaries=[],this.pendingResponses.push({resolve:e=>{for(const s of n)s.resolve(e)},reject:e=>{for(const s of n)s.reject(e)}}),this.queue.push(o),i.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 o=0;o<e.length;o++)e[o].text&&s.push(`${o+1}. ${e[o].text}`),t.push(...e[o].images)}return{text:s.join("\n"),images:t}}ensureInitialized(){if(this.initialized)return;this.initialized=!0;const s=this.piProviderConfig?"pi":"claudecode";i.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(),i.info(`[${this.sessionKey}] Pi engine initialized: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}catch(s){i.error(`[${this.sessionKey}] Failed to initialize Pi engine: ${s}`),i.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 i.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}),i.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(i.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(" "),i.info(`[${this.sessionKey}] Compact: ${this.currentResponse}`)}else if("init"!==t&&"status"!==t){const{type:e,...o}=s;this.currentResponse=JSON.stringify(o,null,2),i.info(`[${this.sessionKey}] System message (${t??"unknown"}): ${this.currentResponse.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 o=s.map(e=>e.type).join(", ");i.debug(`[${this.sessionKey}] SDK assistant message: blocks=[${o}], text length=${t.length}: ${this.config.verboseDebugLogs?t:t.slice(0,15)+"..."}`);s.some(e=>"tool_use"===e.type)&&this.pendingTextBlock&&this.textBlockStreamer&&await this.flushPendingTextBlock();for(const e of s)if("tool_use"===e.type){const s=JSON.stringify(e.input);i.debug(`[${this.sessionKey}] Tool call: ${e.name} ${this.config.verboseDebugLogs?s:s.slice(0,100)+(s.length>100?"...":"")}`),this.toolUseNotifier&&"AskUserQuestion"!==e.name&&this.notifyToolUse(e.name).catch(e=>{i.error(`[${this.sessionKey}] Tool use notification error: ${e}`)})}}if("tool_progress"===e.type){const s=e;i.debug(`[${this.sessionKey}] Tool progress: ${s.tool_name} (${s.elapsed_time_seconds}s)`)}if("result"===e.type){const s=e;let t;i.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 o=s.stop_reason??null;if("success"===s.subtype){if(s.result)this.currentResponse=s.result;else if(!this.currentResponse&&(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,o]of Object.entries(s.modelUsage)){const s=o,i=[` ${t}:`];s.inputTokens&&i.push(`input=${s.inputTokens}`),s.outputTokens&&i.push(`output=${s.outputTokens}`),s.cacheReadInputTokens&&i.push(`cache_read=${s.cacheReadInputTokens}`),s.cacheCreationInputTokens&&i.push(`cache_create=${s.cacheCreationInputTokens}`),void 0!==s.costUSD&&i.push(`cost=$${Number(s.costUSD).toFixed(4)}`),e.push(i.join(" "))}e.length>0&&(this.currentResponse=e.join("\n"))}if(!s.result&&!this.currentResponse){const e=this.piProviderConfig;i.warn(`[${this.sessionKey}] Empty response on success: provider=${e?.provider??"sdk"}, modelId=${e?.modelId??"n/a"}, stop_reason=${o}. Check provider routing and API key.`)}"refusal"===o?(i.warn(`[${this.sessionKey}] Model refused the request`),this.currentResponse||(this.currentResponse="I'm unable to fulfill this request.")):"max_tokens"===o&&i.warn(`[${this.sessionKey}] Response truncated: output token limit reached`)}else if("error_max_turns"===s.subtype)t="max_turns",i.warn(`[${this.sessionKey}] Max turns reached`);else if("error_max_budget_usd"===s.subtype)t="max_budget",i.warn(`[${this.sessionKey}] Max budget reached`);else{const e=s.errors??[];e.some(e=>e.includes("aborted"))?i.info(`[${this.sessionKey}] Request aborted (steer interrupt)`):i.error(`[${this.sessionKey}] SDK error: ${JSON.stringify(s)}`)}const n=this.pendingResponses.shift();if(n){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)),i.info(`[${this.sessionKey}] Response ready: session=${this.currentSessionId}, length=${s.length}${this.streamedAny?` (streamed, full=${e.length})`:""}`),n.resolve({response:s,fullResponse:this.streamedAny?e:void 0,sessionId:this.currentSessionId,sessionReset:!1,errorType:t,stopReason:o})}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){i.error(`[${this.sessionKey}] Output stream error: ${e}`);const s=this.pendingResponses.shift();s&&(this.currentSessionId?(i.warn(`[${this.sessionKey}] Session corrupted: ${this.currentSessionId}`),s.resolve({response:"",sessionId:"",sessionReset:!0})):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}}}
@@ -17,6 +17,7 @@ export type ModelRegistryProvider = () => ModelEntry[];
17
17
  export type PicoAgentProvider = () => PicoAgentInfo;
18
18
  /** Returns true when the session was reset (engine type change). */
19
19
  export type ModelSetHandler = (sessionKey: string, modelId: string) => Promise<boolean>;
20
+ export type DefaultModelSetHandler = (modelId: string) => Promise<void>;
20
21
  /** Filter models eligible for agent use. Includes internal, proxied external, and pico-agent external models. */
21
22
  export declare function filterEligibleModels(models: ModelEntry[], pico?: PicoAgentInfo): ModelEntry[];
22
23
  export declare class ModelCommand implements Command {
@@ -28,4 +29,13 @@ export declare class ModelCommand implements Command {
28
29
  constructor(registryProvider: ModelRegistryProvider, setHandler: ModelSetHandler, picoAgentProvider?: PicoAgentProvider | undefined);
29
30
  execute(ctx: CommandContext): Promise<CommandResult>;
30
31
  }
32
+ export declare class DefaultModelCommand implements Command {
33
+ private registryProvider;
34
+ private setHandler;
35
+ private picoAgentProvider?;
36
+ name: string;
37
+ description: string;
38
+ constructor(registryProvider: ModelRegistryProvider, setHandler: DefaultModelSetHandler, picoAgentProvider?: PicoAgentProvider | undefined);
39
+ execute(ctx: CommandContext): Promise<CommandResult>;
40
+ }
31
41
  //# sourceMappingURL=model.d.ts.map
@@ -1 +1 @@
1
- export function filterEligibleModels(e,t){const n=new Set;if(t?.enabled)for(const e of t.modelRefs){const t=e.split(":")[0]?.trim();t&&n.add(t)}return e.filter(e=>{const i=e.types||["external"];return i.includes("internal")||i.includes("external")&&e.proxy&&"not-used"!==e.proxy||t?.enabled&&i.includes("external")&&n.has(e.name)})}export class ModelCommand{registryProvider;setHandler;picoAgentProvider;name="model";description="Switch model for this session (usage: /model <name or id>)";constructor(e,t,n){this.registryProvider=e,this.setHandler=t,this.picoAgentProvider=n}async execute(e){const t=e.args.trim();if(!t)return{text:"Usage: /model <name or id>\nUse /models to see available models."};const n=this.picoAgentProvider?.(),i=filterEligibleModels(this.registryProvider(),n),o=t.toLowerCase();let s=i.find(e=>e.name===t)??i.find(e=>e.id===t)??i.find(e=>e.name.toLowerCase()===o)??i.find(e=>e.id.toLowerCase()===o);if(!s){const e=i.filter(e=>e.name.toLowerCase().includes(o)||e.id.toLowerCase().includes(o));if(1===e.length)s=e[0];else if(e.length>1){const n=e.map(e=>` ${e.name} (${e.id})`).join("\n");return{text:`"${t}" matches multiple models:\n${n}\n\nPlease be more specific.`}}}if(!s){const e=i.map(e=>` ${e.name} (${e.id})`).join("\n");return{text:`Model "${t}" not found.\n\nAvailable models:\n${e}`}}const r=await this.setHandler(e.sessionKey,s.id),d=`Model switched to: ${s.name} (${s.id})`;return{text:r?`${d}\nSession reset. Starting a new conversation.`:d}}}
1
+ export function filterEligibleModels(e,t){const n=new Set;if(t?.enabled)for(const e of t.modelRefs){const t=e.split(":")[0]?.trim();t&&n.add(t)}return e.filter(e=>{const i=e.types||["external"];return i.includes("internal")||i.includes("external")&&e.proxy&&"not-used"!==e.proxy||t?.enabled&&i.includes("external")&&n.has(e.name)})}export class ModelCommand{registryProvider;setHandler;picoAgentProvider;name="model";description="Switch model for this session (usage: /model <name or id>)";constructor(e,t,n){this.registryProvider=e,this.setHandler=t,this.picoAgentProvider=n}async execute(e){const t=e.args.trim();if(!t)return{text:"Usage: /model <name or id>\nUse /models to see available models."};const n=this.picoAgentProvider?.(),i=filterEligibleModels(this.registryProvider(),n),s=t.toLowerCase();let o=i.find(e=>e.name===t)??i.find(e=>e.id===t)??i.find(e=>e.name.toLowerCase()===s)??i.find(e=>e.id.toLowerCase()===s);if(!o){const e=i.filter(e=>e.name.toLowerCase().includes(s)||e.id.toLowerCase().includes(s));if(1===e.length)o=e[0];else if(e.length>1){const n=e.map(e=>` ${e.name} (${e.id})`).join("\n");return{text:`"${t}" matches multiple models:\n${n}\n\nPlease be more specific.`}}}if(!o){const e=i.map(e=>` ${e.name} (${e.id})`).join("\n");return{text:`Model "${t}" not found.\n\nAvailable models:\n${e}`}}const r=await this.setHandler(e.sessionKey,o.id),d=`Model for this session switched to: ${o.name} (${o.id})`;return{text:r?`${d}\nSession reset. Starting a new conversation.`:d}}}export class DefaultModelCommand{registryProvider;setHandler;picoAgentProvider;name="defaultmodel";description="Set the default model for new sessions (usage: /defaultmodel <name or id>)";constructor(e,t,n){this.registryProvider=e,this.setHandler=t,this.picoAgentProvider=n}async execute(e){const t=e.args.trim();if(!t)return{text:"Usage: /defaultmodel <name or id>\nUse /models to see available models."};const n=this.picoAgentProvider?.(),i=filterEligibleModels(this.registryProvider(),n),s=t.toLowerCase();let o=i.find(e=>e.name===t)??i.find(e=>e.id===t)??i.find(e=>e.name.toLowerCase()===s)??i.find(e=>e.id.toLowerCase()===s);if(!o){const e=i.filter(e=>e.name.toLowerCase().includes(s)||e.id.toLowerCase().includes(s));if(1===e.length)o=e[0];else if(e.length>1){const n=e.map(e=>` ${e.name} (${e.id})`).join("\n");return{text:`"${t}" matches multiple models:\n${n}\n\nPlease be more specific.`}}}if(!o){const e=i.map(e=>` ${e.name} (${e.id})`).join("\n");return{text:`Model "${t}" not found.\n\nAvailable models:\n${e}`}}return await this.setHandler(o.id),{text:`Default model set to: ${o.name} (${o.id})\nNew sessions will use this model.`}}}
package/dist/server.js CHANGED
@@ -1 +1 @@
1
- import{readFileSync as e,writeFileSync as t,mkdirSync as s,existsSync as n}from"node:fs";import{join as r,resolve as i}from"node:path";import{TokenDB as o}from"./auth/token-db.js";import{NodeSignatureDB as a}from"./auth/node-signature-db.js";import{SessionDB as h}from"./agent/session-db.js";import{ChannelManager as c}from"./gateway/channel-manager.js";import{TelegramChannel as g}from"./gateway/channels/telegram.js";import{WhatsAppChannel as m}from"./gateway/channels/whatsapp.js";import{WebChatChannel as l}from"./gateway/channels/webchat.js";import{ResponsesChannel as d}from"./channels/responses.js";import{AgentService as f}from"./agent/agent-service.js";import{SessionManager as u}from"./agent/session-manager.js";import{buildPrompt as p,buildSystemPrompt as b}from"./agent/prompt-builder.js";import{ensureWorkspaceFiles as S,loadWorkspaceFiles as y}from"./agent/workspace-files.js";import{NodeRegistry as w}from"./gateway/node-registry.js";import{MemoryManager as v}from"./memory/memory-manager.js";import{MessageProcessor as M}from"./media/message-processor.js";import{loadSTTProvider as R}from"./stt/stt-loader.js";import{CommandRegistry as C}from"./commands/command-registry.js";import{NewCommand as A}from"./commands/new.js";import{CompactCommand as T}from"./commands/compact.js";import{ModelCommand as k}from"./commands/model.js";import{StopCommand as j}from"./commands/stop.js";import{HelpCommand as $}from"./commands/help.js";import{McpCommand as x}from"./commands/mcp.js";import{ModelsCommand as E}from"./commands/models.js";import{CoderCommand as D}from"./commands/coder.js";import{SandboxCommand as I}from"./commands/sandbox.js";import{SubAgentsCommand as _}from"./commands/subagents.js";import{CustomSubAgentsCommand as N}from"./commands/customsubagents.js";import{StatusCommand as U}from"./commands/status.js";import{ShowToolCommand as P}from"./commands/showtool.js";import{UsageCommand as K}from"./commands/usage.js";import{CronService as H}from"./cron/cron-service.js";import{stripHeartbeatToken as O,isHeartbeatContentEffectivelyEmpty as B}from"./cron/heartbeat-token.js";import{createServerToolsServer as F}from"./tools/server-tools.js";import{createCronToolsServer as L}from"./tools/cron-tools.js";import{createTTSToolsServer as Q}from"./tools/tts-tools.js";import{createMemoryToolsServer as W}from"./tools/memory-tools.js";import{createBrowserToolsServer as z}from"./tools/browser-tools.js";import{BrowserService as G}from"./browser/browser-service.js";import{MemorySearch as q}from"./memory/memory-search.js";import{stripMediaLines as V}from"./utils/media-response.js";import{loadConfig as J,loadRawConfig as X,backupConfig as Y,resolveModelEntry as Z,modelRefName as ee}from"./config.js";import{stringify as te}from"yaml";import{createLogger as se}from"./utils/logger.js";import{SessionErrorHandler as ne}from"./agent/session-error-handler.js";const re=se("Server");export class Server{config;tokenDb;sessionDb;nodeSignatureDb;channelManager;agentService;sessionManager;memoryManager=null;nodeRegistry;messageProcessor;commandRegistry;serverToolsServer;coderSkill;showToolUse=!1;subagentsEnabled=!0;customSubAgentsEnabled=!1;chatSettings=new Map;cronService=null;browserService;memorySearch=null;whatsappQr=null;whatsappConnected=!1;whatsappError;webChatChannel=null;autoRenewTimer=null;constructor(e){this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.tokenDb=new o(e.dbPath),this.sessionDb=new h(e.dbPath),this.nodeSignatureDb=new a(e.dbPath),this.nodeRegistry=new w,this.sessionManager=new u(this.sessionDb),e.memory.enabled&&(this.memoryManager=new v(e.memoryDir));const t=R(e),n=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new M(t,n),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new c(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.serverToolsServer=F(()=>this.triggerRestart(),e.timezone),this.cronService=this.createCronService();const i=this.createMemorySearch(e);this.browserService=new G;const g=e.browser?.enabled?z({nodeRegistry:this.nodeRegistry,config:e}):void 0,m=this.cronService?L(this.cronService,()=>this.config):void 0,l=e.tts.enabled?Q(()=>this.config):void 0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsServer,m,this.sessionDb,l,i,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,g),S(e.dataDir),s(r(e.agent.workspacePath,".claude","skills"),{recursive:!0}),s(r(e.agent.workspacePath,".claude","commands"),{recursive:!0}),s(r(e.agent.workspacePath,".plugins"),{recursive:!0})}getChatSetting(e,t){return this.chatSettings.get(e)?.[t]}setChatSetting(e,t,s){const n=this.chatSettings.get(e)??{};n[t]=s,this.chatSettings.set(e,n)}createCronService(){return new H({storePath:this.config.cronStorePath,enabled:this.config.cron.enabled,defaultTimezone:this.config.timezone,onExecute:e=>this.executeCronJob(e)})}createMemorySearch(e){if(!e.memory.enabled||!e.memory.search.enabled)return;const t=e.memory.search,s=Z(e,t.modelRef),n=(s?.useEnvVar?process.env[s.useEnvVar]:s?.apiKey)||"",r=s?.baseURL||"";if(n)return this.memorySearch=new q(e.memoryDir,e.dataDir,{apiKey:n,baseURL:r||void 0,embeddingModel:t.embeddingModel,embeddingDimensions:t.embeddingDimensions,prefixQuery:t.prefixQuery,prefixDocument:t.prefixDocument,updateDebounceMs:t.updateDebounceMs,embedIntervalMs:t.embedIntervalMs,maxResults:t.maxResults,maxSnippetChars:t.maxSnippetChars,maxInjectedChars:t.maxInjectedChars,rrfK:t.rrfK}),W(this.memorySearch);re.warn(`Memory search enabled but no API key found for modelRef "${t.modelRef}". Search will not start.`)}stopMemorySearch(){this.memorySearch&&(this.memorySearch.stop(),this.memorySearch=null)}collectBroadcastTargets(){const e=new Set,t=[],s=(s,n)=>{const r=`${s}:${n}`;e.has(r)||(e.add(r),t.push({channel:s,chatId:n}))},n=this.config.channels;for(const[e,t]of Object.entries(n)){if("responses"===e)continue;if(!t?.enabled||!this.channelManager.getAdapter(e))continue;const n=t.accounts;if(n)for(const t of Object.values(n)){const n=t?.allowFrom;if(Array.isArray(n))for(const t of n){const n=String(t).trim();n&&s(e,n)}}}for(const e of this.sessionDb.listSessions()){const t=e.sessionKey.indexOf(":");if(t<0)continue;const n=e.sessionKey.substring(0,t),r=e.sessionKey.substring(t+1);"cron"!==n&&r&&(this.channelManager.getAdapter(n)&&s(n,r))}return t}async executeCronJob(t){const s=this.config.cron.broadcastEvents;if(!s&&!this.channelManager.getAdapter(t.channel))return re.warn(`Cron job "${t.name}": skipped (channel "${t.channel}" is not active)`),{response:"",delivered:!1};if(t.suppressToken&&"__heartbeat"===t.name){const s=r(this.config.dataDir,"HEARTBEAT.md");if(n(s))try{const n=e(s,"utf-8");if(B(n))return re.info(`Cron job "${t.name}": skipped (HEARTBEAT.md is empty)`),{response:"",delivered:!1}}catch{}}const i="boolean"==typeof t.isolated?t.isolated:this.config.cron.isolated,o=i?"cron":t.channel,a=i?t.name:t.chatId;re.info(`Cron job "${t.name}": session=${o}:${a}, delivery=${t.channel}:${t.chatId}${s?" (broadcast)":""}`);const h={chatId:a,userId:"cron",channelName:o,text:t.message,attachments:[]},c=await this.handleMessage(h);let g=c;if(t.suppressToken){const e=this.config.cron.heartbeat.ackMaxChars,{shouldSkip:s,text:n}=O(c,e);if(s)return re.info(`Cron job "${t.name}": response suppressed (HEARTBEAT_OK)`),{response:c,delivered:!1};g=n}if(s){const e=this.collectBroadcastTargets();re.info(`Cron job "${t.name}": broadcasting to ${e.length} target(s)`),await Promise.allSettled(e.map(e=>this.channelManager.sendResponse(e.channel,e.chatId,g)))}else await this.channelManager.sendResponse(t.channel,t.chatId,g);return{response:g,delivered:!0}}setupCommands(){this.commandRegistry.register(new A),this.commandRegistry.register(new T);const e=()=>this.config.agent.picoAgent??{enabled:!1,modelRefs:[]};this.commandRegistry.register(new k(()=>this.config.models??[],async(e,s)=>{const n=this.config.models?.find(e=>e.id===s),r=this.config.agent.picoAgent,o=e=>!(!r?.enabled||!Array.isArray(r.modelRefs))&&r.modelRefs.some(t=>t.split(":")[0]===e),a=this.config.agent.model,h=Z(this.config,a),c=o(h?.name??ee(a)),g=o(n?.name??s);this.sessionManager.setModel(e,s);const m=n?`${n.name}:${n.id}`:s;if(this.config.agent.model=m,r?.enabled&&Array.isArray(r.modelRefs)){const e=n?.name??s,t=r.modelRefs.findIndex(t=>t.split(":")[0]===e);if(t>0){const[e]=r.modelRefs.splice(t,1);r.modelRefs.unshift(e)}}this.agentService.destroySession(e);const l=c||g;l&&this.sessionManager.resetSession(e);try{const e=i(process.cwd(),"config.yaml"),s=X(e);s.agent||(s.agent={}),s.agent.model=m,r?.enabled&&Array.isArray(r.modelRefs)&&(s.agent.picoAgent||(s.agent.picoAgent={}),s.agent.picoAgent.modelRefs=[...r.modelRefs]),Y(e),t(e,te(s),"utf-8")}catch{}return l},e)),this.commandRegistry.register(new E(()=>this.config.models??[],e)),this.commandRegistry.register(new D(e=>this.getChatSetting(e,"coderSkill")??this.coderSkill,(e,t)=>this.setChatSetting(e,"coderSkill",t))),this.commandRegistry.register(new I(e=>this.getChatSetting(e,"sandboxEnabled")??!1,(e,t)=>this.setChatSetting(e,"sandboxEnabled",t))),this.commandRegistry.register(new P(e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,(e,t)=>this.setChatSetting(e,"showToolUse",t))),this.commandRegistry.register(new _(e=>this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,(e,t)=>this.setChatSetting(e,"subagentsEnabled",t))),this.commandRegistry.register(new N(e=>this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,(e,t)=>this.setChatSetting(e,"customSubAgentsEnabled",t),()=>this.config)),this.commandRegistry.register(new U(e=>{const t=this.sessionManager.getModel(e)||this.config.agent.model,s=this.config.agent.mainFallback,n=Z(this.config,t),r=s?Z(this.config,s):void 0;return{agentModel:n?.id??t,agentModelName:n?.name??ee(t),fallbackModel:r?.id??s,fallbackModelName:r?.name??(s?ee(s):void 0),coderSkill:this.getChatSetting(e,"coderSkill")??this.coderSkill,showToolUse:this.getChatSetting(e,"showToolUse")??this.showToolUse,subagentsEnabled:this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,customSubAgentsEnabled:this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,sandboxEnabled:this.getChatSetting(e,"sandboxEnabled")??!1,connectedNodes:this.nodeRegistry.listNodes().map(e=>({nodeId:e.nodeId,displayName:e.displayName,hostname:e.hostname}))}})),this.commandRegistry.register(new j(e=>this.agentService.interrupt(e))),this.commandRegistry.register(new x(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new $(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new K(e=>this.agentService.getUsage(e)))}registerChannels(){if(this.config.channels.telegram.enabled){const e=this.config.channels.telegram.accounts;for(const[t,s]of Object.entries(e)){if(!s.botToken){re.error(`Telegram account "${t}" has no botToken configured — skipping. Check your config.yaml.`);continue}const e=new g(s,this.tokenDb,this.config.agent.inflightTyping);this.channelManager.registerAdapter(e)}}if(this.config.channels.whatsapp.enabled){const e=this.config.channels.whatsapp.accounts;for(const[t,s]of Object.entries(e)){const e=new m(s,this.config.agent.inflightTyping);e.setQrCallback((e,t,s)=>{this.whatsappQr=e?{dataUrl:e,timestamp:Date.now()}:null,this.whatsappConnected=t,this.whatsappError=s}),this.channelManager.registerAdapter(e)}}if(this.config.channels.responses.enabled){const e=new d({host:this.config.host,port:this.config.channels.responses.port},this.tokenDb);this.channelManager.registerAdapter(e)}this.webChatChannel||(this.webChatChannel=new l),this.channelManager.registerAdapter(this.webChatChannel)}async handleMessage(e){const t=`${e.channelName}:${e.chatId}`,s=e.text?e.text.length>15?e.text.slice(0,15)+"...":e.text:"[no text]";re.info(`Message from ${t} (user=${e.userId}, ${e.username??"?"}): ${s}`),this.config.verboseDebugLogs&&re.debug(`Message from ${t} full text: ${e.text??"[no text]"}`);try{if(e.text){if(e.text.startsWith("__ask:")){const s=e.text.substring(6);return this.agentService.resolveQuestion(t,s),""}if(this.agentService.hasPendingQuestion(t)){const s=e.text.trim();return this.agentService.resolveQuestion(t,s),`Selected: ${s}`}if(e.text.startsWith("__tool_perm:")){const s="__tool_perm:approve"===e.text;return this.agentService.resolvePermission(t,s),s?"Tool approved.":"Tool denied."}if(this.agentService.hasPendingPermission(t)){const s=e.text.trim().toLowerCase();if("approve"===s||"approva"===s)return this.agentService.resolvePermission(t,!0),"Tool approved.";if("deny"===s||"vieta"===s||"blocca"===s)return this.agentService.resolvePermission(t,!1),"Tool denied."}}const s=!0===e.__passthrough;if(!s&&e.text&&this.commandRegistry.isCommand(e.text)){const s=await this.commandRegistry.dispatch(e.text,{sessionKey:t,chatId:e.chatId,channelName:e.channelName,userId:e.userId});if(s)return s.passthrough?this.handleMessage({...e,text:s.passthrough,__passthrough:!0}):(s.resetSession?(this.agentService.destroySession(t),this.sessionManager.resetSession(t),this.memoryManager&&this.memoryManager.clearSession(t)):s.resetAgent&&this.agentService.destroySession(t),s.text)}if(!s&&e.text?.startsWith("/")&&this.agentService.isBusy(t))return"I'm busy right now. Please resend this request later.";const n=this.sessionManager.getOrCreate(t),r=await this.messageProcessor.process(e),i=p(r,void 0,{sessionKey:t,channel:e.channelName,chatId:e.chatId});re.debug(`[${t}] Prompt to agent (${i.text.length} chars): ${this.config.verboseDebugLogs?i.text:i.text.slice(0,15)+"..."}${i.images.length>0?` [+${i.images.length} image(s)]`:""}`);const o=n.model,a={sessionKey:t,channel:e.channelName,chatId:e.chatId,sessionId:n.sessionId??"",memoryFile:this.memoryManager?this.memoryManager.getConversationFile(t):"",attachmentsDir:this.memoryManager?this.memoryManager.getAttachmentsDir(t):""},h=y(this.config.dataDir),c={config:this.config,sessionContext:a,workspaceFiles:h,mode:"full",hasNodeTools:this.agentService.hasNodeTools(),hasMessageTools:this.agentService.hasMessageTools(),coderSkill:this.getChatSetting(t,"coderSkill")??this.coderSkill,toolServers:this.agentService.getToolServers()},g=b(c),m=b({...c,mode:"minimal"});re.debug(`[${t}] System prompt (${g.length} chars): ${this.config.verboseDebugLogs?g:g.slice(0,15)+"..."}`);try{const s=await this.agentService.sendMessage(t,i,n.sessionId,g,m,o,this.getChatSetting(t,"coderSkill")??this.coderSkill,this.getChatSetting(t,"subagentsEnabled")??this.subagentsEnabled,this.getChatSetting(t,"customSubAgentsEnabled")??this.customSubAgentsEnabled,this.getChatSetting(t,"sandboxEnabled")??!1);if(s.sessionReset){if("[AGENT_CLOSED]"===s.response)return re.info(`[${t}] Agent closed during restart, keeping session ID for resume on next message`),"";{const e={sessionKey:t,sessionId:n.sessionId,error:new Error("Session corruption detected"),timestamp:new Date},s=ne.analyzeError(e.error,e),r=ne.getRecoveryStrategy(s);return re.warn(`[${t}] ${r.message}`),this.sessionManager.updateSessionId(t,""),r.clearSession&&(this.agentService.destroySession(t),this.memoryManager&&this.memoryManager.clearSession(t)),"[SESSION_CORRUPTED] The previous session is no longer valid. Use /new to start a new one."}}if(re.debug(`[${t}] Response from agent (session=${s.sessionId}, len=${s.response.length}): ${this.config.verboseDebugLogs?s.response:s.response.slice(0,15)+"..."}`),s.sessionId&&this.sessionManager.updateSessionId(t,s.sessionId),this.memoryManager&&"cron"!==e.userId){const e=(i.text||"[media]").trim();await this.memoryManager.append(t,"user",e,r.savedFiles.length>0?r.savedFiles:void 0),await this.memoryManager.append(t,"assistant",V(s.fullResponse??s.response))}if("max_turns"===s.errorType){const e="\n\n[MAX_TURNS] The agent reached the maximum number of turns. You can continue the conversation by sending another message.";return s.response?s.response+e:e.trim()}if("max_budget"===s.errorType){const e="\n\n[MAX_BUDGET] The agent reached the maximum budget for this request.";return s.response?s.response+e:e.trim()}if("refusal"===s.stopReason){const e="\n\n[REFUSAL] The model declined to fulfill this request.";return s.response?s.response+e:e.trim()}if("max_tokens"===s.stopReason){const e="\n\n[TRUNCATED] The response was cut short because it exceeded the output token limit. You can ask me to continue.";return s.response?s.response+e:e.trim()}return s.response}catch(e){const s=e instanceof Error?e.message:String(e);return s.includes("SessionAgent closed")||s.includes("agent closed")?(re.info(`[${t}] Agent closed during restart, keeping session ID for resume on next message`),""):(re.error(`Agent error for ${t}: ${e}`),`Error: ${s}`)}}finally{await this.channelManager.releaseTyping(e.channelName,e.chatId).catch(()=>{})}}async start(){re.info("Starting GrabMeABeer server...");const e=[];this.config.channels.telegram.enabled&&e.push("telegram"),this.config.channels.responses.enabled&&e.push("responses"),this.config.channels.whatsapp.enabled&&e.push("whatsapp"),this.config.channels.discord.enabled&&e.push("discord"),this.config.channels.slack.enabled&&e.push("slack"),re.info(`Enabled channels: ${e.join(", ")||"none"}`),await this.channelManager.startAll(),await this.browserService.start(this.config.browser).catch(e=>{re.warn(`Browser service failed to start: ${e}`)}),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),re.info("Server started successfully"),this.notifyAllChannels("Gateway started. Agent is alive!").catch(()=>{})}async initCronAndHeartbeat(){if(!this.cronService)return;await this.cronService.start();const e=this.config.cron.heartbeat,t=(await this.cronService.list({includeDisabled:!0})).find(e=>"__heartbeat"===e.name),s=!!e.channel&&!!this.channelManager.getAdapter(e.channel),n=!!e.message&&e.message.trim().length>=15;if(e.enabled&&e.channel&&e.chatId&&s&&n){const s={schedule:{kind:"every",everyMs:e.every},channel:e.channel,chatId:e.chatId,message:e.message};if(t){const e=t.schedule;(t.channel!==s.channel||t.chatId!==s.chatId||t.message!==s.message||t.isolated!==this.config.cron.isolated||"every"!==e.kind||"every"===e.kind&&e.everyMs!==s.schedule.everyMs||!t.enabled)&&(await this.cronService.update(t.id,{...s,isolated:this.config.cron.isolated,enabled:!0}),re.info("Heartbeat job updated from config"))}else await this.cronService.add({name:"__heartbeat",description:"Auto-generated heartbeat job",enabled:!0,isolated:this.config.cron.isolated,suppressToken:!0,...s}),re.info("Heartbeat job auto-added")}else t&&t.enabled&&(await this.cronService.update(t.id,{enabled:!1}),e.enabled&&!n?re.warn("Heartbeat job disabled: message is empty or too short (minimum 15 characters)"):e.enabled&&!s?re.warn(`Heartbeat job disabled: channel "${e.channel}" is not active`):re.info("Heartbeat job disabled (config changed)"))}getConfig(){return this.config}getTokenDb(){return this.tokenDb}getSessionDb(){return this.sessionDb}getMemoryManager(){return this.memoryManager}getNodeRegistry(){return this.nodeRegistry}getNodeSignatureDb(){return this.nodeSignatureDb}getChannelManager(){return this.channelManager}getAgentService(){return this.agentService}getCronService(){return this.cronService}getCoderSkill(){return this.coderSkill}getWhatsAppQrState(){return{dataUrl:this.whatsappQr?.dataUrl??null,connected:this.whatsappConnected,error:this.whatsappError}}getWebChatChannel(){return this.webChatChannel}async triggerRestart(){re.info("Trigger restart requested");const e=J();await this.reconfigure(e),this.notifyAllChannels("Gateway restarted. Agent is alive!").catch(()=>{})}async notifyAllChannels(e){const t=this.collectBroadcastTargets();re.info(`Broadcasting to ${t.length} target(s)`),await Promise.allSettled(t.map(async t=>{try{await this.channelManager.sendSystemMessage(t.channel,t.chatId,e),await this.channelManager.clearTyping(t.channel,t.chatId),re.debug(`Notified ${t.channel}:${t.chatId}`)}catch(e){re.warn(`Failed to notify ${t.channel}:${t.chatId}: ${e}`)}}))}static AUTO_RENEW_CHECK_INTERVAL_MS=9e5;startAutoRenewTimer(){this.stopAutoRenewTimer();const e=this.config.agent.autoRenew;e&&(re.info(`AutoRenew enabled — resetting sessions inactive for ${e}h (checking every 15 min)`),this.autoRenewTimer=setInterval(()=>{this.autoRenewStaleSessions().catch(e=>re.error(`AutoRenew error: ${e}`))},Server.AUTO_RENEW_CHECK_INTERVAL_MS))}stopAutoRenewTimer(){this.autoRenewTimer&&(clearInterval(this.autoRenewTimer),this.autoRenewTimer=null)}async autoRenewStaleSessions(){const e=this.config.agent.autoRenew;if(!e)return;const t=60*e*60*1e3,s=this.sessionDb.listStaleSessions(t);if(0!==s.length){re.info(`AutoRenew: found ${s.length} stale session(s)`);for(const t of s){const s=t.sessionKey;if(s.startsWith("cron:"))continue;if(this.agentService.isBusy(s))continue;this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s);const n=s.indexOf(":");if(n>0){const t=s.substring(0,n),r=s.substring(n+1),i=this.channelManager.getAdapter(t);if(i)try{await i.sendText(r,`Session renewed automatically after ${e}h of inactivity. Starting fresh!`)}catch(e){re.warn(`AutoRenew: failed to send courtesy message to ${s}: ${e}`)}}re.info(`AutoRenew: session reset — ${s}`)}}}async reconfigure(e){re.info("Reconfiguring server..."),this.cronService&&this.cronService.stop(),await this.channelManager.stopAll(),this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.sessionManager=new u(this.sessionDb),e.memory.enabled?this.memoryManager=new v(e.memoryDir):this.memoryManager=null;const t=R(e),s=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new M(t,s),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new c(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.agentService.destroyAll(),this.serverToolsServer=F(()=>this.triggerRestart(),e.timezone),this.cronService=this.createCronService(),this.stopMemorySearch();const n=this.createMemorySearch(e);await this.browserService.reconfigure(e.browser);const r=e.browser?.enabled?z({nodeRegistry:this.nodeRegistry,config:e}):void 0,i=this.cronService?L(this.cronService,()=>this.config):void 0,o=e.tts.enabled?Q(()=>this.config):void 0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsServer,i,this.sessionDb,o,n,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,r),S(e.dataDir),await this.channelManager.startAll(),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),re.info("Server reconfigured successfully")}async stop(){re.info("Shutting down..."),this.stopAutoRenewTimer(),this.cronService&&this.cronService.stop(),this.memorySearch&&this.memorySearch.stop(),await this.browserService.stop(),this.agentService.destroyAll(),await this.channelManager.stopAll(),this.sessionManager.destroy(),this.sessionDb.close(),this.tokenDb.close(),this.nodeSignatureDb.close(),re.info("Server stopped")}}
1
+ import{readFileSync as e,writeFileSync as t,mkdirSync as s,existsSync as n}from"node:fs";import{join as o,resolve as i}from"node:path";import{TokenDB as r}from"./auth/token-db.js";import{NodeSignatureDB as a}from"./auth/node-signature-db.js";import{SessionDB as c}from"./agent/session-db.js";import{ChannelManager as h}from"./gateway/channel-manager.js";import{TelegramChannel as g}from"./gateway/channels/telegram.js";import{WhatsAppChannel as l}from"./gateway/channels/whatsapp.js";import{WebChatChannel as m}from"./gateway/channels/webchat.js";import{ResponsesChannel as d}from"./channels/responses.js";import{AgentService as f}from"./agent/agent-service.js";import{SessionManager as u}from"./agent/session-manager.js";import{buildPrompt as p,buildSystemPrompt as b}from"./agent/prompt-builder.js";import{ensureWorkspaceFiles as S,loadWorkspaceFiles as y}from"./agent/workspace-files.js";import{NodeRegistry as w}from"./gateway/node-registry.js";import{MemoryManager as v}from"./memory/memory-manager.js";import{MessageProcessor as M}from"./media/message-processor.js";import{loadSTTProvider as R}from"./stt/stt-loader.js";import{CommandRegistry as C}from"./commands/command-registry.js";import{NewCommand as A}from"./commands/new.js";import{CompactCommand as T}from"./commands/compact.js";import{ModelCommand as k,DefaultModelCommand as j}from"./commands/model.js";import{StopCommand as x}from"./commands/stop.js";import{HelpCommand as $}from"./commands/help.js";import{McpCommand as D}from"./commands/mcp.js";import{ModelsCommand as I}from"./commands/models.js";import{CoderCommand as E}from"./commands/coder.js";import{SandboxCommand as _}from"./commands/sandbox.js";import{SubAgentsCommand as N}from"./commands/subagents.js";import{CustomSubAgentsCommand as P}from"./commands/customsubagents.js";import{StatusCommand as U}from"./commands/status.js";import{ShowToolCommand as K}from"./commands/showtool.js";import{UsageCommand as F}from"./commands/usage.js";import{CronService as O}from"./cron/cron-service.js";import{stripHeartbeatToken as H,isHeartbeatContentEffectivelyEmpty as B}from"./cron/heartbeat-token.js";import{createServerToolsServer as L}from"./tools/server-tools.js";import{createCronToolsServer as Q}from"./tools/cron-tools.js";import{createTTSToolsServer as W}from"./tools/tts-tools.js";import{createMemoryToolsServer as z}from"./tools/memory-tools.js";import{createBrowserToolsServer as G}from"./tools/browser-tools.js";import{createPicoToolsServer as q}from"./tools/pico-tools.js";import{BrowserService as V}from"./browser/browser-service.js";import{MemorySearch as J}from"./memory/memory-search.js";import{stripMediaLines as X}from"./utils/media-response.js";import{loadConfig as Y,loadRawConfig as Z,backupConfig as ee,resolveModelEntry as te,modelRefName as se}from"./config.js";import{stringify as ne}from"yaml";import{createLogger as oe}from"./utils/logger.js";import{SessionErrorHandler as ie}from"./agent/session-error-handler.js";const re=oe("Server");export class Server{config;tokenDb;sessionDb;nodeSignatureDb;channelManager;agentService;sessionManager;memoryManager=null;nodeRegistry;messageProcessor;commandRegistry;serverToolsServer;coderSkill;showToolUse=!1;subagentsEnabled=!0;customSubAgentsEnabled=!1;chatSettings=new Map;cronService=null;browserService;memorySearch=null;whatsappQr=null;whatsappConnected=!1;whatsappError;webChatChannel=null;autoRenewTimer=null;constructor(e){this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.tokenDb=new r(e.dbPath),this.sessionDb=new c(e.dbPath),this.nodeSignatureDb=new a(e.dbPath),this.nodeRegistry=new w,this.sessionManager=new u(this.sessionDb),e.memory.enabled&&(this.memoryManager=new v(e.memoryDir));const t=R(e),n=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new M(t,n),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.serverToolsServer=L(()=>this.triggerRestart(),e.timezone),this.cronService=this.createCronService();const i=this.createMemorySearch(e);this.browserService=new V;const g=e.browser?.enabled?G({nodeRegistry:this.nodeRegistry,config:e}):void 0,l=this.cronService?Q(this.cronService,()=>this.config):void 0,m=e.tts.enabled?W(()=>this.config):void 0,d=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0?q({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=y(this.config.dataDir);return b({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsServer,l,this.sessionDb,m,i,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,g,d),S(e.dataDir),s(o(e.agent.workspacePath,".claude","skills"),{recursive:!0}),s(o(e.agent.workspacePath,".claude","commands"),{recursive:!0}),s(o(e.agent.workspacePath,".plugins"),{recursive:!0})}getChatSetting(e,t){return this.chatSettings.get(e)?.[t]}setChatSetting(e,t,s){const n=this.chatSettings.get(e)??{};n[t]=s,this.chatSettings.set(e,n)}createCronService(){return new O({storePath:this.config.cronStorePath,enabled:this.config.cron.enabled,defaultTimezone:this.config.timezone,onExecute:e=>this.executeCronJob(e)})}createMemorySearch(e){if(!e.memory.enabled||!e.memory.search.enabled)return;const t=e.memory.search,s=te(e,t.modelRef),n=(s?.useEnvVar?process.env[s.useEnvVar]:s?.apiKey)||"",o=s?.baseURL||"";if(n)return this.memorySearch=new J(e.memoryDir,e.dataDir,{apiKey:n,baseURL:o||void 0,embeddingModel:t.embeddingModel,embeddingDimensions:t.embeddingDimensions,prefixQuery:t.prefixQuery,prefixDocument:t.prefixDocument,updateDebounceMs:t.updateDebounceMs,embedIntervalMs:t.embedIntervalMs,maxResults:t.maxResults,maxSnippetChars:t.maxSnippetChars,maxInjectedChars:t.maxInjectedChars,rrfK:t.rrfK}),z(this.memorySearch);re.warn(`Memory search enabled but no API key found for modelRef "${t.modelRef}". Search will not start.`)}stopMemorySearch(){this.memorySearch&&(this.memorySearch.stop(),this.memorySearch=null)}collectBroadcastTargets(){const e=new Set,t=[],s=(s,n)=>{const o=`${s}:${n}`;e.has(o)||(e.add(o),t.push({channel:s,chatId:n}))},n=this.config.channels;for(const[e,t]of Object.entries(n)){if("responses"===e)continue;if(!t?.enabled||!this.channelManager.getAdapter(e))continue;const n=t.accounts;if(n)for(const t of Object.values(n)){const n=t?.allowFrom;if(Array.isArray(n))for(const t of n){const n=String(t).trim();n&&s(e,n)}}}for(const e of this.sessionDb.listSessions()){const t=e.sessionKey.indexOf(":");if(t<0)continue;const n=e.sessionKey.substring(0,t),o=e.sessionKey.substring(t+1);"cron"!==n&&o&&(this.channelManager.getAdapter(n)&&s(n,o))}return t}async executeCronJob(t){const s=this.config.cron.broadcastEvents;if(!s&&!this.channelManager.getAdapter(t.channel))return re.warn(`Cron job "${t.name}": skipped (channel "${t.channel}" is not active)`),{response:"",delivered:!1};if(t.suppressToken&&"__heartbeat"===t.name){const s=o(this.config.dataDir,"HEARTBEAT.md");if(n(s))try{const n=e(s,"utf-8");if(B(n))return re.info(`Cron job "${t.name}": skipped (HEARTBEAT.md is empty)`),{response:"",delivered:!1}}catch{}}const i="boolean"==typeof t.isolated?t.isolated:this.config.cron.isolated,r=i?"cron":t.channel,a=i?t.name:t.chatId;re.info(`Cron job "${t.name}": session=${r}:${a}, delivery=${t.channel}:${t.chatId}${s?" (broadcast)":""}`);const c={chatId:a,userId:"cron",channelName:r,text:t.message,attachments:[]},h=await this.handleMessage(c);let g=h;if(t.suppressToken){const e=this.config.cron.heartbeat.ackMaxChars,{shouldSkip:s,text:n}=H(h,e);if(s)return re.info(`Cron job "${t.name}": response suppressed (HEARTBEAT_OK)`),{response:h,delivered:!1};g=n}if(s){const e=this.collectBroadcastTargets();re.info(`Cron job "${t.name}": broadcasting to ${e.length} target(s)`),await Promise.allSettled(e.map(e=>this.channelManager.sendResponse(e.channel,e.chatId,g)))}else await this.channelManager.sendResponse(t.channel,t.chatId,g);return{response:g,delivered:!0}}setupCommands(){this.commandRegistry.register(new A),this.commandRegistry.register(new T);const e=()=>this.config.agent.picoAgent??{enabled:!1,modelRefs:[]};this.commandRegistry.register(new k(()=>this.config.models??[],async(e,t)=>{const s=this.config.models?.find(e=>e.id===t),n=this.config.agent.picoAgent,o=e=>!(!n?.enabled||!Array.isArray(n.modelRefs))&&n.modelRefs.some(t=>t.split(":")[0]===e),i=this.sessionManager.getModel(e)||this.config.agent.model,r=te(this.config,i),a=o(r?.name??se(i)),c=o(s?.name??t);this.sessionManager.setModel(e,t),this.agentService.destroySession(e);const h=a||c;return h&&this.sessionManager.resetSession(e),h},e)),this.commandRegistry.register(new j(()=>this.config.models??[],async e=>{const s=this.config.models?.find(t=>t.id===e),n=s?`${s.name}:${s.id}`:e;this.config.agent.model=n;const o=this.config.agent.picoAgent;if(o?.enabled&&Array.isArray(o.modelRefs)){const t=s?.name??e,n=o.modelRefs.findIndex(e=>e.split(":")[0]===t);if(n>0){const[e]=o.modelRefs.splice(n,1);o.modelRefs.unshift(e)}}try{const e=i(process.cwd(),"config.yaml"),s=Z(e);s.agent||(s.agent={}),s.agent.model=n,o?.enabled&&Array.isArray(o.modelRefs)&&(s.agent.picoAgent||(s.agent.picoAgent={}),s.agent.picoAgent.modelRefs=[...o.modelRefs]),ee(e),t(e,ne(s),"utf-8")}catch{}},e)),this.commandRegistry.register(new I(()=>this.config.models??[],e)),this.commandRegistry.register(new E(e=>this.getChatSetting(e,"coderSkill")??this.coderSkill,(e,t)=>this.setChatSetting(e,"coderSkill",t))),this.commandRegistry.register(new _(e=>this.getChatSetting(e,"sandboxEnabled")??!1,(e,t)=>this.setChatSetting(e,"sandboxEnabled",t))),this.commandRegistry.register(new K(e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,(e,t)=>this.setChatSetting(e,"showToolUse",t))),this.commandRegistry.register(new N(e=>this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,(e,t)=>this.setChatSetting(e,"subagentsEnabled",t))),this.commandRegistry.register(new P(e=>this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,(e,t)=>this.setChatSetting(e,"customSubAgentsEnabled",t),()=>this.config)),this.commandRegistry.register(new U(e=>{const t=this.sessionManager.getModel(e)||this.config.agent.model,s=this.config.agent.mainFallback,n=te(this.config,t),o=s?te(this.config,s):void 0;return{agentModel:n?.id??t,agentModelName:n?.name??se(t),fallbackModel:o?.id??s,fallbackModelName:o?.name??(s?se(s):void 0),coderSkill:this.getChatSetting(e,"coderSkill")??this.coderSkill,showToolUse:this.getChatSetting(e,"showToolUse")??this.showToolUse,subagentsEnabled:this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,customSubAgentsEnabled:this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,sandboxEnabled:this.getChatSetting(e,"sandboxEnabled")??!1,connectedNodes:this.nodeRegistry.listNodes().map(e=>({nodeId:e.nodeId,displayName:e.displayName,hostname:e.hostname}))}})),this.commandRegistry.register(new x(e=>this.agentService.interrupt(e))),this.commandRegistry.register(new D(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new $(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new F(e=>this.agentService.getUsage(e)))}registerChannels(){if(this.config.channels.telegram.enabled){const e=this.config.channels.telegram.accounts;for(const[t,s]of Object.entries(e)){if(!s.botToken){re.error(`Telegram account "${t}" has no botToken configured — skipping. Check your config.yaml.`);continue}const e=new g(s,this.tokenDb,this.config.agent.inflightTyping);this.channelManager.registerAdapter(e)}}if(this.config.channels.whatsapp.enabled){const e=this.config.channels.whatsapp.accounts;for(const[t,s]of Object.entries(e)){const e=new l(s,this.config.agent.inflightTyping);e.setQrCallback((e,t,s)=>{this.whatsappQr=e?{dataUrl:e,timestamp:Date.now()}:null,this.whatsappConnected=t,this.whatsappError=s}),this.channelManager.registerAdapter(e)}}if(this.config.channels.responses.enabled){const e=new d({host:this.config.host,port:this.config.channels.responses.port},this.tokenDb);this.channelManager.registerAdapter(e)}this.webChatChannel||(this.webChatChannel=new m),this.channelManager.registerAdapter(this.webChatChannel)}async handleMessage(e){const t=`${e.channelName}:${e.chatId}`,s=e.text?e.text.length>15?e.text.slice(0,15)+"...":e.text:"[no text]";re.info(`Message from ${t} (user=${e.userId}, ${e.username??"?"}): ${s}`),this.config.verboseDebugLogs&&re.debug(`Message from ${t} full text: ${e.text??"[no text]"}`);try{if(e.text){if(e.text.startsWith("__ask:")){const s=e.text.substring(6);return this.agentService.resolveQuestion(t,s),""}if(this.agentService.hasPendingQuestion(t)){const s=e.text.trim();return this.agentService.resolveQuestion(t,s),`Selected: ${s}`}if(e.text.startsWith("__tool_perm:")){const s="__tool_perm:approve"===e.text;return this.agentService.resolvePermission(t,s),s?"Tool approved.":"Tool denied."}if(this.agentService.hasPendingPermission(t)){const s=e.text.trim().toLowerCase();if("approve"===s||"approva"===s)return this.agentService.resolvePermission(t,!0),"Tool approved.";if("deny"===s||"vieta"===s||"blocca"===s)return this.agentService.resolvePermission(t,!1),"Tool denied."}}const s=!0===e.__passthrough;if(!s&&e.text&&this.commandRegistry.isCommand(e.text)){const s=await this.commandRegistry.dispatch(e.text,{sessionKey:t,chatId:e.chatId,channelName:e.channelName,userId:e.userId});if(s)return s.passthrough?this.handleMessage({...e,text:s.passthrough,__passthrough:!0}):(s.resetSession?(this.agentService.destroySession(t),this.sessionManager.resetSession(t),this.memoryManager&&this.memoryManager.clearSession(t)):s.resetAgent&&this.agentService.destroySession(t),s.text)}if(!s&&e.text?.startsWith("/")&&this.agentService.isBusy(t))return"I'm busy right now. Please resend this request later.";const n=this.sessionManager.getOrCreate(t),o=await this.messageProcessor.process(e),i=p(o,void 0,{sessionKey:t,channel:e.channelName,chatId:e.chatId});re.debug(`[${t}] Prompt to agent (${i.text.length} chars): ${this.config.verboseDebugLogs?i.text:i.text.slice(0,15)+"..."}${i.images.length>0?` [+${i.images.length} image(s)]`:""}`);const r=n.model,a={sessionKey:t,channel:e.channelName,chatId:e.chatId,sessionId:n.sessionId??"",memoryFile:this.memoryManager?this.memoryManager.getConversationFile(t):"",attachmentsDir:this.memoryManager?this.memoryManager.getAttachmentsDir(t):""},c=y(this.config.dataDir),h={config:this.config,sessionContext:a,workspaceFiles:c,mode:"full",hasNodeTools:this.agentService.hasNodeTools(),hasMessageTools:this.agentService.hasMessageTools(),coderSkill:this.getChatSetting(t,"coderSkill")??this.coderSkill,toolServers:this.agentService.getToolServers()},g=b(h),l=b({...h,mode:"minimal"});re.debug(`[${t}] System prompt (${g.length} chars): ${this.config.verboseDebugLogs?g:g.slice(0,15)+"..."}`);try{const s=await this.agentService.sendMessage(t,i,n.sessionId,g,l,r,this.getChatSetting(t,"coderSkill")??this.coderSkill,this.getChatSetting(t,"subagentsEnabled")??this.subagentsEnabled,this.getChatSetting(t,"customSubAgentsEnabled")??this.customSubAgentsEnabled,this.getChatSetting(t,"sandboxEnabled")??!1);if(s.sessionReset){if("[AGENT_CLOSED]"===s.response)return re.info(`[${t}] Agent closed during restart, keeping session ID for resume on next message`),"";{const e={sessionKey:t,sessionId:n.sessionId,error:new Error("Session corruption detected"),timestamp:new Date},s=ie.analyzeError(e.error,e),o=ie.getRecoveryStrategy(s);return re.warn(`[${t}] ${o.message}`),this.sessionManager.updateSessionId(t,""),o.clearSession&&(this.agentService.destroySession(t),this.memoryManager&&this.memoryManager.clearSession(t)),"[SESSION_CORRUPTED] The previous session is no longer valid. Use /new to start a new one."}}if(re.debug(`[${t}] Response from agent (session=${s.sessionId}, len=${s.response.length}): ${this.config.verboseDebugLogs?s.response:s.response.slice(0,15)+"..."}`),s.sessionId&&this.sessionManager.updateSessionId(t,s.sessionId),this.memoryManager&&"cron"!==e.userId){const e=(i.text||"[media]").trim();await this.memoryManager.append(t,"user",e,o.savedFiles.length>0?o.savedFiles:void 0),await this.memoryManager.append(t,"assistant",X(s.fullResponse??s.response))}if("max_turns"===s.errorType){const e="\n\n[MAX_TURNS] The agent reached the maximum number of turns. You can continue the conversation by sending another message.";return s.response?s.response+e:e.trim()}if("max_budget"===s.errorType){const e="\n\n[MAX_BUDGET] The agent reached the maximum budget for this request.";return s.response?s.response+e:e.trim()}if("refusal"===s.stopReason){const e="\n\n[REFUSAL] The model declined to fulfill this request.";return s.response?s.response+e:e.trim()}if("max_tokens"===s.stopReason){const e="\n\n[TRUNCATED] The response was cut short because it exceeded the output token limit. You can ask me to continue.";return s.response?s.response+e:e.trim()}return s.response}catch(e){const s=e instanceof Error?e.message:String(e);return s.includes("SessionAgent closed")||s.includes("agent closed")?(re.info(`[${t}] Agent closed during restart, keeping session ID for resume on next message`),""):(re.error(`Agent error for ${t}: ${e}`),`Error: ${s}`)}}finally{await this.channelManager.releaseTyping(e.channelName,e.chatId).catch(()=>{})}}async start(){re.info("Starting GrabMeABeer server...");const e=[];this.config.channels.telegram.enabled&&e.push("telegram"),this.config.channels.responses.enabled&&e.push("responses"),this.config.channels.whatsapp.enabled&&e.push("whatsapp"),this.config.channels.discord.enabled&&e.push("discord"),this.config.channels.slack.enabled&&e.push("slack"),re.info(`Enabled channels: ${e.join(", ")||"none"}`),await this.channelManager.startAll(),await this.browserService.start(this.config.browser).catch(e=>{re.warn(`Browser service failed to start: ${e}`)}),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),re.info("Server started successfully"),this.notifyAllChannels("Gateway started. Agent is alive!").catch(()=>{})}async initCronAndHeartbeat(){if(!this.cronService)return;await this.cronService.start();const e=this.config.cron.heartbeat,t=(await this.cronService.list({includeDisabled:!0})).find(e=>"__heartbeat"===e.name),s=!!e.channel&&!!this.channelManager.getAdapter(e.channel),n=!!e.message&&e.message.trim().length>=15;if(e.enabled&&e.channel&&e.chatId&&s&&n){const s={schedule:{kind:"every",everyMs:e.every},channel:e.channel,chatId:e.chatId,message:e.message};if(t){const e=t.schedule;(t.channel!==s.channel||t.chatId!==s.chatId||t.message!==s.message||t.isolated!==this.config.cron.isolated||"every"!==e.kind||"every"===e.kind&&e.everyMs!==s.schedule.everyMs||!t.enabled)&&(await this.cronService.update(t.id,{...s,isolated:this.config.cron.isolated,enabled:!0}),re.info("Heartbeat job updated from config"))}else await this.cronService.add({name:"__heartbeat",description:"Auto-generated heartbeat job",enabled:!0,isolated:this.config.cron.isolated,suppressToken:!0,...s}),re.info("Heartbeat job auto-added")}else t&&t.enabled&&(await this.cronService.update(t.id,{enabled:!1}),e.enabled&&!n?re.warn("Heartbeat job disabled: message is empty or too short (minimum 15 characters)"):e.enabled&&!s?re.warn(`Heartbeat job disabled: channel "${e.channel}" is not active`):re.info("Heartbeat job disabled (config changed)"))}getConfig(){return this.config}getTokenDb(){return this.tokenDb}getSessionDb(){return this.sessionDb}getMemoryManager(){return this.memoryManager}getNodeRegistry(){return this.nodeRegistry}getNodeSignatureDb(){return this.nodeSignatureDb}getChannelManager(){return this.channelManager}getAgentService(){return this.agentService}getCronService(){return this.cronService}getCoderSkill(){return this.coderSkill}getWhatsAppQrState(){return{dataUrl:this.whatsappQr?.dataUrl??null,connected:this.whatsappConnected,error:this.whatsappError}}getWebChatChannel(){return this.webChatChannel}async triggerRestart(){re.info("Trigger restart requested");const e=Y();await this.reconfigure(e),this.notifyAllChannels("Gateway restarted. Agent is alive!").catch(()=>{})}async notifyAllChannels(e){const t=this.collectBroadcastTargets();re.info(`Broadcasting to ${t.length} target(s)`),await Promise.allSettled(t.map(async t=>{try{await this.channelManager.sendSystemMessage(t.channel,t.chatId,e),await this.channelManager.clearTyping(t.channel,t.chatId),re.debug(`Notified ${t.channel}:${t.chatId}`)}catch(e){re.warn(`Failed to notify ${t.channel}:${t.chatId}: ${e}`)}}))}static AUTO_RENEW_CHECK_INTERVAL_MS=9e5;startAutoRenewTimer(){this.stopAutoRenewTimer();const e=this.config.agent.autoRenew;e&&(re.info(`AutoRenew enabled — resetting sessions inactive for ${e}h (checking every 15 min)`),this.autoRenewTimer=setInterval(()=>{this.autoRenewStaleSessions().catch(e=>re.error(`AutoRenew error: ${e}`))},Server.AUTO_RENEW_CHECK_INTERVAL_MS))}stopAutoRenewTimer(){this.autoRenewTimer&&(clearInterval(this.autoRenewTimer),this.autoRenewTimer=null)}async autoRenewStaleSessions(){const e=this.config.agent.autoRenew;if(!e)return;const t=60*e*60*1e3,s=this.sessionDb.listStaleSessions(t);if(0!==s.length){re.info(`AutoRenew: found ${s.length} stale session(s)`);for(const t of s){const s=t.sessionKey;if(s.startsWith("cron:"))continue;if(this.agentService.isBusy(s))continue;this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s);const n=s.indexOf(":");if(n>0){const t=s.substring(0,n),o=s.substring(n+1),i=this.channelManager.getAdapter(t);if(i)try{await i.sendText(o,`Session renewed automatically after ${e}h of inactivity. Starting fresh!`)}catch(e){re.warn(`AutoRenew: failed to send courtesy message to ${s}: ${e}`)}}re.info(`AutoRenew: session reset — ${s}`)}}}async reconfigure(e){re.info("Reconfiguring server..."),this.cronService&&this.cronService.stop(),await this.channelManager.stopAll(),this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.sessionManager=new u(this.sessionDb),e.memory.enabled?this.memoryManager=new v(e.memoryDir):this.memoryManager=null;const t=R(e),s=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new M(t,s),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.agentService.destroyAll(),this.serverToolsServer=L(()=>this.triggerRestart(),e.timezone),this.cronService=this.createCronService(),this.stopMemorySearch();const n=this.createMemorySearch(e);await this.browserService.reconfigure(e.browser);const o=e.browser?.enabled?G({nodeRegistry:this.nodeRegistry,config:e}):void 0,i=this.cronService?Q(this.cronService,()=>this.config):void 0,r=e.tts.enabled?W(()=>this.config):void 0,a=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0?q({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=y(this.config.dataDir);return b({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsServer,i,this.sessionDb,r,n,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,o,a),S(e.dataDir),await this.channelManager.startAll(),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),re.info("Server reconfigured successfully")}async stop(){re.info("Shutting down..."),this.stopAutoRenewTimer(),this.cronService&&this.cronService.stop(),this.memorySearch&&this.memorySearch.stop(),await this.browserService.stop(),this.agentService.destroyAll(),await this.channelManager.stopAll(),this.sessionManager.destroy(),this.sessionDb.close(),this.tokenDb.close(),this.nodeSignatureDb.close(),re.info("Server stopped")}}
@@ -0,0 +1,11 @@
1
+ import type { AppConfig } from "../config.js";
2
+ export interface PicoToolsConfig {
3
+ /** Live config accessor */
4
+ getConfig: () => AppConfig;
5
+ /** System prompt to use for the subagent (minimal/subagent mode) */
6
+ getSubagentSystemPrompt: () => string;
7
+ /** MCP server options passed to the caller's session (for tool forwarding) */
8
+ getCallerMcpOptions: () => Record<string, unknown>;
9
+ }
10
+ export declare function createPicoToolsServer(cfg: PicoToolsConfig): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
11
+ //# sourceMappingURL=pico-tools.d.ts.map
@@ -0,0 +1 @@
1
+ import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as o}from"zod";import{createLogger as n}from"../utils/logger.js";const r=n("PicoTools");function s(e,t){const o=e.split(":");if(o.length<2)return null;const n=o[0].trim();let r,s;if(o.length>=3)r=o[1].trim(),s=o.slice(2).join(":").trim(),s.startsWith(r+":")&&(s=s.substring(r.length+1));else{const e=o[1].trim(),t=e.indexOf("/");t>0?(r=e.substring(0,t),s=e.substring(t+1)):(r="openrouter",s=e)}const i=t.models?.find(e=>e.name===n);return i?.baseURL&&i.baseURL.includes("openrouter.ai")&&"openrouter"!==r&&(s=`${r}/${s}`,r="openrouter"),{name:n,provider:r,modelId:s,apiKey:i?.apiKey||void 0,contextWindowTokens:i?.contextWindow||void 0}}export function createPicoToolsServer(n){return e({name:"pico-tools",version:"1.0.0",tools:[t("pico_models","List available Pico Agent (Pi engine) models that can be invoked as subagents via pico_query. Returns model name, provider, and model ID for each configured Pi engine model.",{},async()=>{const e=n.getConfig(),t=e.agent.picoAgent;if(!t?.enabled||!Array.isArray(t.modelRefs)||0===t.modelRefs.length)return{content:[{type:"text",text:"Pico Agent is not enabled or has no models configured."}]};const o=t.modelRefs.map(t=>s(t,e)).filter(e=>null!==e).map(e=>({name:e.name,provider:e.provider,modelId:e.modelId,ref:`${e.provider}/${e.modelId}`}));return 0===o.length?{content:[{type:"text",text:"No valid Pi engine models found in picoAgent.modelRefs."}]}:{content:[{type:"text",text:JSON.stringify(o,null,2)}]}}),t("pico_query","Invoke a Pico Agent (Pi engine) model as a subagent. Sends a one-shot prompt and returns the response. Use pico_models first to see available models. The subagent can optionally use the same tools as the caller.",{model:o.string().describe("Model name from pico_models (e.g. 'Gemini Flash'). Must match a name from pico_models."),prompt:o.string().describe("The prompt/task to send to the subagent"),useTools:o.boolean().optional().describe("If true, the subagent gets the same tools as the caller. If false (default), text-in/text-out only."),maxTurns:o.number().optional().describe("Maximum number of agent turns (default: 10)"),timeoutMs:o.number().optional().describe("Timeout in milliseconds (default: 120000 = 2 min)")},async e=>{const t=n.getConfig(),o=t.agent.picoAgent;if(!o?.enabled||!Array.isArray(o.modelRefs))return{content:[{type:"text",text:"Pico Agent is not enabled."}],isError:!0};const i=o.modelRefs.find(t=>t.split(":")[0]===e.model);if(!i){const c=o.modelRefs.map(e=>e.split(":")[0]).join(", ");return{content:[{type:"text",text:`Model "${e.model}" not found. Available: ${c}`}],isError:!0}}const a=s(i,t);if(!a)return{content:[{type:"text",text:`Invalid modelRef format: ${i}`}],isError:!0};const l=e.timeoutMs??12e4,m=e.maxTurns??10,d=e.useTools??!1;r.info(`pico_query: model=${a.name} (${a.provider}/${a.modelId}), tools=${d}, maxTurns=${m}, timeout=${l}ms`);try{const p=await import("../pi-agent-provider/index.js"),u={provider:a.provider,modelId:a.modelId,apiKey:a.apiKey,contextWindowTokens:a.contextWindowTokens},f={systemPrompt:n.getSubagentSystemPrompt(),maxTurns:m,cwd:t.agent.workspacePath,permissionMode:"bypassPermissions",allowDangerouslySkipPermissions:!0};d&&(f.mcpServers=n.getCallerMcpOptions());const g=await p.createToolRegistryFromOptions(f),y={type:"user",message:{role:"user",content:e.prompt}};async function*x(){yield y}const b=p.piQuery({prompt:x(),options:f},u,g);let h="",$="",v=0;const T=new Promise(e=>setTimeout(()=>e("timeout"),l)),P=(async()=>{for await(const e of b)if("assistant"===e.type){const t=e.message?.content;if(Array.isArray(t))for(const e of t)"text"===e.type&&(h+=e.text)}else if("result"===e.type){const t=e;t.result&&(h=t.result),t.total_cost_usd&&(v=t.total_cost_usd),t.errors?.length&&($=t.errors.join("; "))}return"done"})();if("timeout"===await Promise.race([P,T])){b.close(),r.warn(`pico_query: timeout after ${l}ms for model ${a.name}`);const _=h.trim();return{content:[{type:"text",text:_?`[TIMEOUT after ${l}ms] Partial response:\n\n${_}`:`[TIMEOUT] The subagent did not respond within ${l}ms.`}],isError:!_}}if(b.close(),$&&!h.trim())return r.warn(`pico_query: error from ${a.name}: ${$}`),{content:[{type:"text",text:`Subagent error: ${$}`}],isError:!0};const w=v>0?`\n\n[Cost: $${v.toFixed(6)}]`:"";return r.info(`pico_query: response from ${a.name} (${h.length} chars, cost=$${v.toFixed(6)})`),{content:[{type:"text",text:(h.trim()||"(empty response)")+w}]}}catch(A){const I=A instanceof Error?A.message:String(A);return r.error(`pico_query failed: ${I}`),{content:[{type:"text",text:`Failed to invoke subagent: ${I}`}],isError:!0}}})]})}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hera-al/server",
3
- "version": "1.6.4",
3
+ "version": "1.6.5",
4
4
  "private": false,
5
5
  "description": "Hera Artificial Life — Multi-channel AI agent gateway with autonomous capabilities",
6
6
  "license": "MIT",