@hera-al/server 1.6.5 → 1.6.7

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.
@@ -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,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}}}
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,m,f,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),f&&(x["tts-tools"]=f),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"))),m){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);let d,c;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}`);const p=n?.rollingMemoryModel;if(p){const e=p.split(":");if(e.length>=3)d=e[1].trim(),c=e.slice(2).join(":").trim();else if(2===e.length){const s=e[1].indexOf("/");s>0?(d=e[1].substring(0,s).trim(),c=e[1].substring(s+1).trim()):c=e[1].trim()}c&&i.info(`[${this.currentSessionId}] Summarization model resolved: ${d}/${c}`)}return{provider:a,modelId:h,apiKey:u?.apiKey||void 0,baseUrl:u?.baseURL||void 0,contextWindowTokens:u?.contextWindow||void 0,costInput:u?.costInput||void 0,costOutput:u?.costOutput||void 0,costCacheRead:u?.costCacheRead||void 0,costCacheWrite:u?.costCacheWrite||void 0,summarizationProvider:d,summarizationModelId:c}}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 +1 @@
1
- export class HelpCommand{getSdkCommands;name="help";description="Show available commands";constructor(e){this.getSdkCommands=e}async execute(e){const o=["Available commands:","","/coder on|off — Toggle the built-in coder skill in the system prompt","/compact — Compress the current session context to reduce token usage","/customsubagents on|off — Pass custom subagents (configured in Nostromo) to the agent","/help — Show this message","/mcp — List available MCP skills","/model <name> — Switch the agent model for the current session","/models — List all models in the registry","/new — Start a fresh session (clears conversation history and memory)","/sandbox on|off — Toggle sandbox mode for command execution","/showtool on|off — Show tool-use notifications in the chat while the agent works","/status — Show current server state (model, fallback, toggles, connected nodes)","/stop — Interrupt the agent if it is currently processing","/subagents on|off — Enable or disable subagent invocation (removes the Task tool when off)","/usage — Show SDK usage for the current session (similar to /context)"],t=["compact","keybindings-help"],s=this.getSdkCommands();if(s.length>0){const e=s.filter(e=>!t.includes(e)&&!e.startsWith("mcp__")).sort((e,o)=>e.localeCompare(o));if(e.length>0){o.push(""),o.push("Claude SDK commands:");for(const t of e)o.push(` /${t}`)}}return{text:o.join("\n")}}}
1
+ export class HelpCommand{getSdkCommands;name="help";description="Show available commands";constructor(e){this.getSdkCommands=e}async execute(e){const o=["Available commands:","","/coder on|off — Toggle the built-in coder skill in the system prompt","/compact — Compress the current session context to reduce token usage","/customsubagents on|off — Pass custom subagents (configured in Nostromo) to the agent","/defaultmodel <name> — Set the default model for new sessions (persists across restarts)","/help — Show this message","/mcp — List available MCP skills","/model <name> — Switch the agent model for the current session","/models — List all models in the registry","/new — Start a fresh session (clears conversation history and memory)","/sandbox on|off — Toggle sandbox mode for command execution","/showtool on|off — Show tool-use notifications in the chat while the agent works","/status — Show current server state (model, fallback, toggles, connected nodes)","/stop — Interrupt the agent if it is currently processing","/subagents on|off — Enable or disable subagent invocation (removes the Task tool when off)","/usage — Show SDK usage for the current session (similar to /context)"],s=["compact","keybindings-help"],t=this.getSdkCommands();if(t.length>0){const e=t.filter(e=>!s.includes(e)&&!e.startsWith("mcp__")).sort((e,o)=>e.localeCompare(o));if(e.length>0){o.push(""),o.push("Claude SDK commands:");for(const s of e)o.push(` /${s}`)}}return{text:o.join("\n")}}}
@@ -1 +1 @@
1
- export function agentJS(){return"\nvar SA_TOOL_LIST = ['Read','Write','Edit','Bash','Glob','Grep','WebSearch','WebFetch'];\n\nvar _editModelIdx = -1;\n\n/* ---- Models ---- */\nfunction showAddModel(){\n document.getElementById('addModelForm').style.display='';\n // Reset form\n document.getElementById('newModelId').value='';\n document.getElementById('newModelName').value='';\n document.getElementById('newModelBaseURL').value='https://api.openai.com/v1';\n document.getElementById('newModelApiKey').value='';\n document.getElementById('newModelEnvVar').value='';\n document.getElementById('newModelType').value='external';\n document.getElementById('newModelProxy').value='not-used';\n document.getElementById('newModelFastUrl').value='';\n document.getElementById('newModelFastProxyApiKey').value='';\n document.getElementById('newModelContextWindow').value='200000';\n document.getElementById('newModelCostInput').value='0';\n document.getElementById('newModelCostOutput').value='0';\n document.getElementById('newModelCostCacheRead').value='0';\n document.getElementById('newModelCostCacheWrite').value='0';\n updateNewModelApiFields();\n}\nfunction hideAddModel(){ document.getElementById('addModelForm').style.display='none'; }\nfunction sanitizeEnvVarInput(el){\n var v = el.value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'');\n if(v !== el.value) el.value = v;\n}\nfunction updateNewModelApiFields(){\n var type = document.getElementById('newModelType').value;\n document.getElementById('newModelApiFields').style.display = type!=='internal' ? '' : 'none';\n var baseField = document.getElementById('newModelBaseURLField');\n if(baseField) baseField.style.display = type==='external' ? '' : 'none';\n var proxyField = document.getElementById('newModelProxyField');\n if(proxyField) proxyField.style.display = type==='external' ? '' : 'none';\n var extraFields = document.getElementById('newModelExtraFields');\n if(extraFields) extraFields.style.display = type==='external' ? '' : 'none';\n updateNewModelProxyFields();\n}\nfunction updateNewModelProxyFields(){\n var proxy = document.getElementById('newModelProxy').value;\n var enabled = proxy !== 'not-used';\n var fu = document.getElementById('newModelFastUrl');\n var fk = document.getElementById('newModelFastProxyApiKey');\n if(fu) fu.disabled = !enabled;\n if(fk) fk.disabled = !enabled;\n}\nfunction updateEditModelApiFields(){\n var type = document.getElementById('editModelType').value;\n document.getElementById('editModelApiFields').style.display = type!=='internal' ? '' : 'none';\n var baseField = document.getElementById('editModelBaseURLField');\n if(baseField) baseField.style.display = type==='external' ? '' : 'none';\n var proxyField = document.getElementById('editModelProxyField');\n if(proxyField) proxyField.style.display = type==='external' ? '' : 'none';\n var extraFields = document.getElementById('editModelExtraFields');\n if(extraFields) extraFields.style.display = type==='external' ? '' : 'none';\n updateEditModelProxyFields();\n}\nfunction updateEditModelProxyFields(){\n var proxy = document.getElementById('editModelProxy').value;\n var enabled = proxy !== 'not-used';\n var fu = document.getElementById('editModelFastUrl');\n var fk = document.getElementById('editModelFastProxyApiKey');\n if(fu) fu.disabled = !enabled;\n if(fk) fk.disabled = !enabled;\n}\nasync function loadModels(){\n currentConfig = await fetchAPI('/config');\n if(!currentConfig.models) currentConfig.models=[];\n renderModelsTable();\n}\nfunction renderModelsTable(){\n var models = currentConfig.models||[];\n var tbody = document.getElementById('modelsBody');\n tbody.innerHTML='';\n var hasVisible = false;\n for(var i=0;i<models.length;i++){\n var m = models[i];\n var types = m.types||['external'];\n if(types.indexOf('env-var')!==-1) continue;\n hasVisible = true;\n var typeBadges = types.map(function(t){ return '<span class=\"badge badge-blue\" style=\"font-size:11px;padding:1px 6px;margin-right:2px\">'+esc(t)+'</span>'; }).join('');\n var proxyBadge = (m.proxy && m.proxy !== 'not-used') ? ' <span class=\"badge badge-green\" style=\"font-size:11px;padding:1px 6px\">'+esc(m.proxy)+'</span>' : '';\n tbody.innerHTML += '<tr data-model-idx=\"'+i+'\"><td>'+esc(m.name)+'</td><td style=\"font-family:monospace;font-size:13px\">'+esc(m.id)+'</td><td>'+typeBadges+proxyBadge+'</td><td style=\"white-space:nowrap\"><button class=\"btn-ghost btn-sm\" onclick=\"startEditModel('+i+')\">Edit</button> <button class=\"btn-danger btn-sm\" onclick=\"confirmDeleteModel('+i+')\">Delete</button></td></tr>';\n }\n if(!hasVisible){\n tbody.innerHTML='<tr><td colspan=\"4\" style=\"text-align:center;color:var(--text-muted);padding:20px\">No models in registry</td></tr>';\n }\n // Hide edit form when re-rendering\n document.getElementById('editModelForm').style.display='none';\n _editModelIdx = -1;\n}\nfunction addModel(){\n var id = document.getElementById('newModelId').value.trim();\n var name = document.getElementById('newModelName').value.trim();\n if(!id){ toast('Model ID required','err'); return; }\n if(!name) name = id;\n var type = document.getElementById('newModelType').value;\n var types = [type];\n var needsApi = type!=='internal';\n var useEnvVar = needsApi ? document.getElementById('newModelEnvVar').value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'').trim() : '';\n var apiKey = needsApi ? document.getElementById('newModelApiKey').value.trim() : '';\n var baseURL = type==='external' ? document.getElementById('newModelBaseURL').value.trim() : '';\n var proxy = type==='external' ? document.getElementById('newModelProxy').value : 'not-used';\n var fastUrl = (type==='external' && proxy!=='not-used') ? document.getElementById('newModelFastUrl').value.trim() : '';\n var fastProxyApiKey = (type==='external' && proxy!=='not-used') ? document.getElementById('newModelFastProxyApiKey').value.trim() : '';\n var contextWindow = type==='external' ? (parseInt(document.getElementById('newModelContextWindow').value)||200000) : 200000;\n var costInput = type==='external' ? (parseFloat(document.getElementById('newModelCostInput').value)||0) : 0;\n var costOutput = type==='external' ? (parseFloat(document.getElementById('newModelCostOutput').value)||0) : 0;\n var costCacheRead = type==='external' ? (parseFloat(document.getElementById('newModelCostCacheRead').value)||0) : 0;\n var costCacheWrite = type==='external' ? (parseFloat(document.getElementById('newModelCostCacheWrite').value)||0) : 0;\n if(!currentConfig.models) currentConfig.models=[];\n var dup = currentConfig.models.some(function(m){ return m.name===name && m.id===id; });\n if(dup){ toast('A model configuration with the same Model Name and Model ID already exists','err'); return; }\n currentConfig.models.push({id:id, name:name, types:types, proxy:proxy, fastUrl:fastUrl, fastProxyApiKey:fastProxyApiKey, apiKey:apiKey, baseURL:baseURL, useEnvVar:useEnvVar, contextWindow:contextWindow, costInput:costInput, costOutput:costOutput, costCacheRead:costCacheRead, costCacheWrite:costCacheWrite});\n hideAddModel();\n renderModelsTable();\n populateModelSelects();\n saveConfig();\n}\nvar _deleteModelIdx = -1;\nfunction confirmDeleteModel(idx){\n _deleteModelIdx = idx;\n var models = (currentConfig&&currentConfig.models)||[];\n var name = models[idx] ? models[idx].name : 'this model';\n document.getElementById('modelDeleteName').textContent = name;\n document.getElementById('modelDeleteModal').classList.add('open');\n}\nfunction closeModelDeleteModal(){\n document.getElementById('modelDeleteModal').classList.remove('open');\n _deleteModelIdx = -1;\n}\nfunction doDeleteModel(){\n if(_deleteModelIdx>=0 && currentConfig.models){\n currentConfig.models.splice(_deleteModelIdx,1);\n renderModelsTable();\n populateModelSelects();\n saveConfig();\n }\n closeModelDeleteModal();\n}\nfunction startEditModel(idx){\n if(!currentConfig.models||!currentConfig.models[idx]) return;\n _editModelIdx = idx;\n var m = currentConfig.models[idx];\n document.getElementById('editModelId').value = m.id||'';\n document.getElementById('editModelName').value = m.name||'';\n document.getElementById('editModelBaseURL').value = m.baseURL||'';\n document.getElementById('editModelApiKey').value = m.apiKey||'';\n document.getElementById('editModelEnvVar').value = m.useEnvVar||'';\n document.getElementById('editModelProxy').value = m.proxy||'not-used';\n document.getElementById('editModelFastUrl').value = m.fastUrl||'';\n document.getElementById('editModelFastProxyApiKey').value = m.fastProxyApiKey||'';\n document.getElementById('editModelContextWindow').value = m.contextWindow||200000;\n document.getElementById('editModelCostInput').value = m.costInput||0;\n document.getElementById('editModelCostOutput').value = m.costOutput||0;\n document.getElementById('editModelCostCacheRead').value = m.costCacheRead||0;\n document.getElementById('editModelCostCacheWrite').value = m.costCacheWrite||0;\n var types = m.types||['external'];\n document.getElementById('editModelType').value = types[0]||'external';\n updateEditModelApiFields();\n // Position edit form after the table\n document.getElementById('editModelForm').style.display='';\n}\nfunction finishEditModel(){\n if(_editModelIdx<0 || !currentConfig.models||!currentConfig.models[_editModelIdx]) return;\n var id = document.getElementById('editModelId').value.trim();\n var name = document.getElementById('editModelName').value.trim();\n if(!id){ toast('Model ID required','err'); return; }\n if(!name) name = id;\n var type = document.getElementById('editModelType').value;\n var types = [type];\n var needsApi = type!=='internal';\n var useEnvVar = needsApi ? document.getElementById('editModelEnvVar').value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'').trim() : '';\n var apiKey = needsApi ? document.getElementById('editModelApiKey').value.trim() : '';\n var baseURL = type==='external' ? document.getElementById('editModelBaseURL').value.trim() : '';\n var proxy = type==='external' ? document.getElementById('editModelProxy').value : 'not-used';\n var fastUrl = (type==='external' && proxy!=='not-used') ? document.getElementById('editModelFastUrl').value.trim() : '';\n var fastProxyApiKey = (type==='external' && proxy!=='not-used') ? document.getElementById('editModelFastProxyApiKey').value.trim() : '';\n var contextWindow = type==='external' ? (parseInt(document.getElementById('editModelContextWindow').value)||200000) : 200000;\n var costInput = type==='external' ? (parseFloat(document.getElementById('editModelCostInput').value)||0) : 0;\n var costOutput = type==='external' ? (parseFloat(document.getElementById('editModelCostOutput').value)||0) : 0;\n var costCacheRead = type==='external' ? (parseFloat(document.getElementById('editModelCostCacheRead').value)||0) : 0;\n var costCacheWrite = type==='external' ? (parseFloat(document.getElementById('editModelCostCacheWrite').value)||0) : 0;\n var dup = currentConfig.models.some(function(m, i){ return i!==_editModelIdx && m.name===name && m.id===id; });\n if(dup){ toast('A model configuration with the same Model Name and Model ID already exists','err'); return; }\n currentConfig.models[_editModelIdx] = {id:id, name:name, types:types, proxy:proxy, fastUrl:fastUrl, fastProxyApiKey:fastProxyApiKey, apiKey:apiKey, baseURL:baseURL, useEnvVar:useEnvVar, contextWindow:contextWindow, costInput:costInput, costOutput:costOutput, costCacheRead:costCacheRead, costCacheWrite:costCacheWrite};\n _editModelIdx = -1;\n renderModelsTable();\n populateModelSelects();\n saveConfig();\n}\nfunction cancelEditModel(){\n _editModelIdx = -1;\n document.getElementById('editModelForm').style.display='none';\n}\nfunction populateModelSelects(){\n var models = (currentConfig&&currentConfig.models)||[];\n var picoEnabled = document.getElementById('picoEnabled') && document.getElementById('picoEnabled').checked;\n var picoNames = {};\n if (picoEnabled) { for (var pi=0; pi<_picoModels.length; pi++) picoNames[_picoModels[pi].name] = true; }\n var internalModels = models.filter(function(m){ var t=m.types||['external']; return t.indexOf('internal')!==-1 || (t.indexOf('external')!==-1 && m.proxy && m.proxy!=='not-used') || (picoEnabled && t.indexOf('external')!==-1 && picoNames[m.name]); });\n var selects = {\n agentModel: {val:'', allowNone:false},\n agentMainFallback: {val:'', allowNone:true}\n };\n for(var key in selects){\n var el = document.getElementById(key);\n if(!el) continue;\n selects[key].val = el.value;\n el.innerHTML='';\n if(selects[key].allowNone) el.innerHTML += '<option value=\"\">None</option>';\n for(var i=0;i<internalModels.length;i++){\n el.innerHTML += '<option value=\"'+esc(internalModels[i].name+':'+internalModels[i].id)+'\">'+esc(internalModels[i].name)+' ('+esc(internalModels[i].id)+')</option>';\n }\n el.value = selects[key].val;\n }\n populateSTTModelSelect();\n populateMemSearchModelSelect();\n populatePicoModelSelect();\n populateRollingModelSelect();\n}\nfunction populateSTTModelSelect(){\n var models = (currentConfig&&currentConfig.models)||[];\n var el = document.getElementById('sttModelRef');\n if(!el) return;\n var prev = el.value;\n el.innerHTML = '<option value=\"\">-- select --</option>';\n for(var i=0;i<models.length;i++){\n var types = models[i].types||['external'];\n if(types.indexOf('external')===-1) continue;\n el.innerHTML += '<option value=\"'+esc(models[i].name+':'+models[i].id)+'\">'+esc(models[i].name)+' ('+esc(models[i].id)+')</option>';\n }\n el.value = prev;\n}\nfunction populateMemSearchModelSelect(){\n var models = (currentConfig&&currentConfig.models)||[];\n var el = document.getElementById('memSearchModelRef');\n if(!el) return;\n var prev = el.value;\n el.innerHTML = '<option value=\"\">-- select --</option>';\n for(var i=0;i<models.length;i++){\n var types = models[i].types||['external'];\n if(types.indexOf('external')===-1) continue;\n el.innerHTML += '<option value=\"'+esc(models[i].name+':'+models[i].id)+'\">'+esc(models[i].name)+' ('+esc(models[i].id)+')</option>';\n }\n el.value = prev;\n}\n\n/* ---- Pico Agent ---- */\nvar _picoModels = []; // [{name, piProvider, piModelId, contextWindow}]\nvar _agentLoading = false; // suppress markDirty during initial load\n\nfunction detectPiProvider(model) {\n var url = (model.baseURL || '').toLowerCase();\n if (url.includes('openrouter.ai')) return 'openrouter';\n if (url.includes('openai.com')) return 'openai';\n if (url.includes('x.ai')) return 'xai';\n if (url.includes('googleapis.com')) return 'google';\n if (url.includes('groq.com')) return 'groq';\n if (url.includes('mistral.ai')) return 'mistral';\n return 'openai';\n}\n\nfunction loadPicoAgent() {\n var a = (currentConfig && currentConfig.agent) || {};\n var pico = a.picoAgent || {};\n // Fallback: migrate from engine if picoAgent absent and engine.type === \"pi\"\n if (!a.picoAgent && a.engine && a.engine.type === 'pi') {\n pico = { enabled: true, modelRefs: [], rollingMemoryModel: '' };\n if (a.engine.piModelRef) {\n pico.modelRefs = [a.engine.piModelRef];\n }\n }\n var el = document.getElementById('picoEnabled');\n if (el) el.checked = !!pico.enabled;\n // Parse modelRefs into _picoModels\n _picoModels = [];\n var refs = pico.modelRefs || [];\n var models = (currentConfig && currentConfig.models) || [];\n for (var i = 0; i < refs.length; i++) {\n var parts = refs[i].split(':');\n var name = parts[0] || '';\n var piProvider = parts.length >= 3 ? parts[1] : '';\n var piModelId = parts.length >= 3 ? parts.slice(2).join(':') : (parts[1] || '');\n // Find matching model entry for contextWindow\n var matched = models.filter(function(m){ return m.name === name; })[0];\n var ctx = (matched && matched.contextWindow) || 200000;\n if (!piProvider && matched) piProvider = detectPiProvider(matched);\n _picoModels.push({ name: name, piProvider: piProvider, piModelId: piModelId, contextWindow: ctx });\n }\n updatePicoFields();\n renderPicoModelList();\n // Set rolling model AFTER populateRollingModelSelect() has built the options\n var rollingEl = document.getElementById('picoRollingModel');\n if (rollingEl) rollingEl.value = pico.rollingMemoryModel || '';\n}\n\nfunction updatePicoFields() {\n var enabled = document.getElementById('picoEnabled').checked;\n var fields = document.getElementById('picoFields');\n if (fields) fields.style.display = enabled ? '' : 'none';\n if (enabled) {\n populatePicoModelSelect();\n populateRollingModelSelect();\n }\n populateModelSelects();\n if (!_agentLoading) markDirty();\n}\n\nfunction populatePicoModelSelect() {\n var models = (currentConfig && currentConfig.models) || [];\n var el = document.getElementById('picoModelSelect');\n if (!el) return;\n el.innerHTML = '<option value=\"\" disabled selected>Select a model...</option>';\n var alreadyAdded = {};\n for (var j = 0; j < _picoModels.length; j++) alreadyAdded[_picoModels[j].name] = true;\n var hasOptions = false;\n for (var i = 0; i < models.length; i++) {\n var types = models[i].types || ['external'];\n if (types.indexOf('external') === -1) continue;\n if (alreadyAdded[models[i].name]) continue;\n el.innerHTML += '<option value=\"' + esc(models[i].name) + '\">' + esc(models[i].name) + ' (' + esc(models[i].id) + ')</option>';\n hasOptions = true;\n }\n if (!hasOptions) {\n el.innerHTML = '<option value=\"\" disabled selected>-- no available models --</option>';\n }\n}\n\nfunction populateRollingModelSelect() {\n var models = (currentConfig && currentConfig.models) || [];\n var el = document.getElementById('picoRollingModel');\n if (!el) return;\n var prev = el.value;\n el.innerHTML = '<option value=\"\">-- none --</option>';\n for (var i = 0; i < models.length; i++) {\n var types = models[i].types || ['external'];\n if (types.indexOf('external') === -1) continue;\n el.innerHTML += '<option value=\"' + esc(models[i].name) + '\">' + esc(models[i].name) + ' (' + esc(models[i].id) + ')</option>';\n }\n el.value = prev;\n}\n\nfunction addPicoModel() {\n var sel = document.getElementById('picoModelSelect');\n var name = sel.value;\n if (!name) return;\n var models = (currentConfig && currentConfig.models) || [];\n var matched = models.filter(function(m){ return m.name === name; })[0];\n if (!matched) return;\n var piProvider = detectPiProvider(matched);\n var piModelId = matched.id || '';\n // Strip registry namespace prefix (e.g. \"openrouter:openai/gpt-5.2\" → \"openai/gpt-5.2\")\n var nsIdx = piModelId.indexOf(':');\n if (nsIdx > 0) piModelId = piModelId.substring(nsIdx + 1);\n var ctx = matched.contextWindow || 200000;\n _picoModels.push({ name: name, piProvider: piProvider, piModelId: piModelId, contextWindow: ctx });\n renderPicoModelList();\n populatePicoModelSelect();\n populateModelSelects();\n markDirty();\n}\n\nfunction removePicoModel(idx) {\n _picoModels.splice(idx, 1);\n renderPicoModelList();\n populatePicoModelSelect();\n populateModelSelects();\n markDirty();\n}\n\nfunction clearRollingModel() {\n var el = document.getElementById('picoRollingModel');\n if (el) el.value = '';\n markDirty();\n}\n\nfunction renderPicoModelList() {\n var container = document.getElementById('picoModelList');\n if (!container) return;\n if (_picoModels.length === 0) {\n container.innerHTML = '<div style=\"color:var(--text-muted);font-size:13px;padding:8px 0\">No models added. Use the select above to add models.</div>';\n return;\n }\n var html = '';\n for (var i = 0; i < _picoModels.length; i++) {\n var m = _picoModels[i];\n var ctxLabel = m.contextWindow >= 1000 ? Math.round(m.contextWindow / 1000) + 'k ctx' : m.contextWindow + ' ctx';\n html += '<div class=\"pico-model-item\" draggable=\"true\" data-pico-idx=\"' + i + '\">';\n html += '<span class=\"pico-drag-handle\">⠇</span>';\n html += '<span class=\"pico-model-name\">' + esc(m.name) + '</span>';\n html += '<span class=\"pico-model-ctx\">' + esc(ctxLabel) + '</span>';\n if (i === 0) html += '<span class=\"badge badge-blue\" style=\"font-size:11px;padding:1px 6px\">default</span>';\n html += '<button class=\"pico-model-remove\" onclick=\"removePicoModel(' + i + ')\">&times;</button>';\n html += '</div>';\n }\n container.innerHTML = html;\n // Attach drag events\n var items = container.querySelectorAll('.pico-model-item');\n items.forEach(function(item) {\n item.addEventListener('dragstart', function(e) {\n e.dataTransfer.setData('text/plain', item.dataset.picoIdx);\n item.classList.add('dragging');\n });\n item.addEventListener('dragend', function() {\n item.classList.remove('dragging');\n });\n item.addEventListener('dragover', function(e) {\n e.preventDefault();\n });\n item.addEventListener('drop', function(e) {\n e.preventDefault();\n var fromIdx = parseInt(e.dataTransfer.getData('text/plain'));\n var toIdx = parseInt(item.dataset.picoIdx);\n if (fromIdx === toIdx) return;\n var moved = _picoModels.splice(fromIdx, 1)[0];\n _picoModels.splice(toIdx, 0, moved);\n renderPicoModelList();\n markDirty();\n });\n });\n}\n\n/* ---- Model ref upgrade ---- */\nfunction upgradeModelRef(val, models){\n if(!val) return val;\n if(val.indexOf(':')!==-1) return val;\n var m = models.filter(function(x){ return x.name===val; })[0];\n return m ? m.name+':'+m.id : val;\n}\n\n/* ---- Agent ---- */\nasync function loadAgent(){\n currentConfig = await fetchAPI('/config');\n const a = currentConfig.agent||{};\n var models = currentConfig.models||[];\n // Load pico agent FIRST so _picoModels is populated before populateModelSelects()\n _agentLoading = true;\n loadPicoAgent();\n populateModelSelects();\n document.getElementById('agentModel').value = upgradeModelRef(a.model||'', models);\n document.getElementById('agentMainFallback').value = upgradeModelRef(a.mainFallback||'', models);\n document.getElementById('agentMaxTurns').value = a.maxTurns||10;\n document.getElementById('agentPermMode').value = a.permissionMode||'default';\n document.getElementById('agentSessionTTL').value = a.sessionTTL||3600;\n document.getElementById('agentSettingSources').value = a.settingSources||'project';\n document.getElementById('agentCoderSkill').checked = !!a.builtinCoderSkill;\n document.getElementById('agentAutoRenew').value = a.autoRenew||0;\n var allowed = a.allowedTools||[];\n document.querySelectorAll('#agentToolsGrid [data-tool]').forEach(function(cb){cb.checked = allowed.indexOf(cb.dataset.tool)!==-1;});\n document.getElementById('agentQueueMode').value = a.queueMode||'collect';\n document.getElementById('agentDebounceMs').value = a.queueDebounceMs!=null ? a.queueDebounceMs : 1500;\n document.getElementById('agentQueueCap').value = a.queueCap!=null ? a.queueCap : 20;\n document.getElementById('agentDropPolicy').value = a.queueDropPolicy||'summarize';\n document.getElementById('agentInflightTyping').checked = a.inflightTyping!==false;\n document.getElementById('agentAutoApprove').checked = a.autoApproveTools!==false;\n updateQueueFields();\n _agentLoading = false;\n sectionsLoaded.agent = true;\n}\n\n/* ---- Queue fields visibility ---- */\nfunction updateQueueFields(){\n var mode = document.getElementById('agentQueueMode').value;\n document.getElementById('queueCollectFields').style.display = mode==='collect' ? '' : 'none';\n}\n\n/* ---- SubAgents ---- */\nvar _saDeleteIdx = -1;\nasync function loadSubAgents(){\n currentConfig = await fetchAPI('/config');\n renderSubAgentCards();\n}\nfunction toggleSaAccordion(idx){\n var el = document.querySelector('.sa-acc[data-sa-idx=\"'+idx+'\"]');\n if(el) el.classList.toggle('open');\n}\nfunction renderSubAgentCards(){\n var sas = (currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents) || [];\n var container = document.getElementById('subAgentCards');\n var empty = document.getElementById('subAgentEmpty');\n if(sas.length === 0){\n container.innerHTML = '';\n empty.style.display = '';\n return;\n }\n empty.style.display = 'none';\n var order = [];\n for(var k=0;k<sas.length;k++) order.push(k);\n order.sort(function(a,b){ return (sas[a].name||'').localeCompare(sas[b].name||''); });\n var html = '';\n for(var oi = 0; oi < order.length; oi++){\n var i = order[oi];\n var sa = sas[i];\n html += '<div class=\"sa-acc\" data-sa-idx=\"'+i+'\">';\n html += '<div class=\"sa-acc-header\" onclick=\"toggleSaAccordion('+i+')\">';\n html += '<svg class=\"sa-acc-chevron\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"6 9 12 15 18 9\"/></svg>';\n html += '<span class=\"sa-acc-name\">' + esc(sa.name) + '</span>';\n if(sa.expandContext) html += '<span class=\"badge badge-blue\" style=\"font-size:10px;padding:1px 6px\">expanded</span>';\n html += '<label class=\"toggle\" onclick=\"event.stopPropagation()\"><input type=\"checkbox\" data-sa-toggle=\"'+i+'\" '+(sa.enabled?'checked':'')+'><span></span></label>';\n html += '<button class=\"btn-danger btn-sm\" onclick=\"event.stopPropagation();confirmDeleteSubAgent('+i+')\">Delete</button>';\n html += '</div>';\n html += '<div class=\"sa-acc-body\">';\n html += '<div class=\"field\"><label>Description</label><textarea data-sa-field=\"'+i+'.description\" rows=\"2\" oninput=\"updateSaField('+i+',&quot;description&quot;,this.value)\">'+esc(sa.description)+'</textarea></div>';\n html += '<div class=\"field\"><label>Prompt</label><textarea data-sa-field=\"'+i+'.prompt\" rows=\"3\" oninput=\"updateSaField('+i+',&quot;prompt&quot;,this.value)\">'+esc(sa.prompt)+'</textarea></div>';\n html += '<div class=\"field\"><label>Model</label><select data-sa-field=\"'+i+'.model\" onchange=\"updateSaField('+i+',&quot;model&quot;,this.value)\">';\n var saModels = ['inherit','sonnet','opus','haiku'];\n for(var j=0;j<saModels.length;j++){\n html += '<option value=\"'+saModels[j]+'\"'+(sa.model===saModels[j]?' selected':'')+'>'+saModels[j]+'</option>';\n }\n html += '</select></div>';\n var saTools = sa.tools||[];\n html += '<div class=\"field\"><label>Tools</label><div style=\"display:flex;flex-wrap:wrap;gap:4px;margin-top:4px\">';\n for(var t=0;t<SA_TOOL_LIST.length;t++){\n var tn = SA_TOOL_LIST[t];\n var checked = saTools.indexOf(tn)!==-1;\n html += '<label class=\"tool-toggle-sm\"><label class=\"toggle-sm\"><input type=\"checkbox\" data-sa-tool=\"'+i+'\" data-tool-name=\"'+tn+'\"'+(checked?' checked':'')+'><span></span></label> '+tn+'</label>';\n }\n html += '</div></div>';\n html += '<div style=\"display:flex;align-items:center;gap:12px;margin-top:8px\"><span style=\"font-size:13px;font-weight:500\">Expand Context</span><label class=\"toggle\"><input type=\"checkbox\" data-sa-expand=\"'+i+'\"'+(sa.expandContext?' checked':'')+'><span></span></label></div>';\n html += '</div></div>';\n }\n container.innerHTML = html;\n // Bind expandContext listeners\n container.querySelectorAll('[data-sa-expand]').forEach(function(cb){\n cb.addEventListener('change', function(){\n var idx = parseInt(cb.dataset.saExpand);\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents && currentConfig.agent.customSubAgents[idx]){\n currentConfig.agent.customSubAgents[idx].expandContext = cb.checked;\n markDirty();\n renderSubAgentCards();\n }\n });\n });\n // Bind toggle listeners\n container.querySelectorAll('[data-sa-toggle]').forEach(function(cb){\n cb.addEventListener('change', function(){\n var idx = parseInt(cb.dataset.saToggle);\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents){\n currentConfig.agent.customSubAgents[idx].enabled = cb.checked;\n markDirty();\n }\n });\n });\n // Bind tool toggle listeners\n container.querySelectorAll('[data-sa-tool]').forEach(function(cb){\n cb.addEventListener('change', function(){\n var idx = parseInt(cb.dataset.saTool);\n var toolName = cb.dataset.toolName;\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents && currentConfig.agent.customSubAgents[idx]){\n var tools = currentConfig.agent.customSubAgents[idx].tools || [];\n if(cb.checked){\n if(tools.indexOf(toolName)===-1) tools.push(toolName);\n } else {\n tools = tools.filter(function(t){return t!==toolName;});\n }\n currentConfig.agent.customSubAgents[idx].tools = tools;\n markDirty();\n }\n });\n });\n}\nfunction confirmDeleteSubAgent(idx){\n _saDeleteIdx = idx;\n var sas = (currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents) || [];\n var name = sas[idx] ? sas[idx].name : 'this subagent';\n document.getElementById('saDeleteName').textContent = name;\n document.getElementById('saDeleteModal').classList.add('open');\n}\nfunction closeSaDeleteModal(){\n document.getElementById('saDeleteModal').classList.remove('open');\n _saDeleteIdx = -1;\n}\nfunction doDeleteSubAgent(){\n if(_saDeleteIdx >= 0) deleteSubAgent(_saDeleteIdx);\n closeSaDeleteModal();\n saveConfig();\n}\nfunction updateSaField(idx, field, value){\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents && currentConfig.agent.customSubAgents[idx]){\n currentConfig.agent.customSubAgents[idx][field] = value;\n markDirty();\n }\n}\nfunction updateSaTools(idx, value){\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents && currentConfig.agent.customSubAgents[idx]){\n currentConfig.agent.customSubAgents[idx].tools = value.split(',').map(function(s){return s.trim()}).filter(Boolean);\n markDirty();\n }\n}\nfunction showAddSubAgent(){\n document.getElementById('addSubAgentForm').style.display = '';\n document.getElementById('newSaName').value = '';\n document.getElementById('newSaDesc').value = '';\n document.getElementById('newSaPrompt').value = '';\n document.getElementById('newSaModel').value = 'inherit';\n document.querySelectorAll('#newSaToolsGrid [data-new-sa-tool]').forEach(function(cb){cb.checked = cb.dataset.newSaTool!=='Bash';});\n document.getElementById('newSaExpandContext').checked = false;\n document.getElementById('newSaName').focus();\n}\nfunction hideAddSubAgent(){\n document.getElementById('addSubAgentForm').style.display = 'none';\n}\nasync function addSubAgent(){\n var name = document.getElementById('newSaName').value.trim();\n var desc = document.getElementById('newSaDesc').value.trim();\n var prompt = document.getElementById('newSaPrompt').value.trim();\n var model = document.getElementById('newSaModel').value;\n var tools = [];\n document.querySelectorAll('#newSaToolsGrid [data-new-sa-tool]').forEach(function(cb){if(cb.checked) tools.push(cb.dataset.newSaTool);});\n if(!name){ toast('Name is required','err'); return; }\n if(!/^[a-zA-Z0-9_\\-\\[\\]!]+$/.test(name)){ toast('Name may only contain a-z A-Z 0-9 - _ [ ] !','err'); return; }\n if(!desc || desc.length < 10){ toast('Description must be at least 10 characters','err'); return; }\n if(!prompt || prompt.length < 10){ toast('Prompt must be at least 10 characters','err'); return; }\n if(!currentConfig){ currentConfig = await fetchAPI('/config'); }\n if(!currentConfig.agent) currentConfig.agent = {};\n if(!currentConfig.agent.customSubAgents) currentConfig.agent.customSubAgents = [];\n var expandContext = document.getElementById('newSaExpandContext').checked;\n currentConfig.agent.customSubAgents.push({ name:name, description:desc, prompt:prompt, model:model, tools:tools, expandContext:expandContext, enabled:false });\n hideAddSubAgent();\n renderSubAgentCards();\n saveConfig();\n}\nfunction deleteSubAgent(idx){\n if(!currentConfig || !currentConfig.agent || !currentConfig.agent.customSubAgents) return;\n currentConfig.agent.customSubAgents.splice(idx, 1);\n renderSubAgentCards();\n markDirty();\n}\n\n/* ---- Vars ---- */\nvar _editVarIdx = -1;\nvar _deleteVarIdx = -1;\n\nfunction showAddVar(){\n document.getElementById('addVarForm').style.display='';\n document.getElementById('newVarName').value='';\n document.getElementById('newVarEnvVar').value='';\n document.getElementById('newVarApiKey').value='';\n}\nfunction hideAddVar(){ document.getElementById('addVarForm').style.display='none'; }\nfunction addVar(){\n var name = document.getElementById('newVarName').value.trim();\n var useEnvVar = document.getElementById('newVarEnvVar').value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'').trim();\n var apiKey = document.getElementById('newVarApiKey').value.trim();\n if(!name){ toast('Display Name required','err'); return; }\n if(!useEnvVar){ toast('Env Var required','err'); return; }\n if(!currentConfig.models) currentConfig.models=[];\n currentConfig.models.push({id:useEnvVar, name:name, types:['env-var'], apiKey:apiKey, baseURL:'', useEnvVar:useEnvVar});\n hideAddVar();\n renderVarsTable();\n saveConfig();\n}\nasync function loadVars(){\n currentConfig = await fetchAPI('/config');\n if(!currentConfig.models) currentConfig.models=[];\n renderVarsTable();\n}\nfunction renderVarsTable(){\n var models = currentConfig.models||[];\n var tbody = document.getElementById('varsBody');\n tbody.innerHTML='';\n var hasVisible = false;\n for(var i=0;i<models.length;i++){\n var m = models[i];\n var types = m.types||['external'];\n if(types.indexOf('env-var')===-1) continue;\n hasVisible = true;\n var envDisplay = m.useEnvVar||m.id||'';\n tbody.innerHTML += '<tr data-var-idx=\"'+i+'\"><td>'+esc(m.name)+'</td><td style=\"font-family:monospace;font-size:13px\">'+esc(envDisplay)+'</td><td style=\"white-space:nowrap\"><button class=\"btn-ghost btn-sm\" onclick=\"startEditVar('+i+')\">Edit</button> <button class=\"btn-danger btn-sm\" onclick=\"confirmDeleteVar('+i+')\">Delete</button></td></tr>';\n }\n if(!hasVisible){\n tbody.innerHTML='<tr><td colspan=\"3\" style=\"text-align:center;color:var(--text-muted);padding:20px\">No vars in registry</td></tr>';\n }\n document.getElementById('editVarForm').style.display='none';\n _editVarIdx = -1;\n}\nfunction startEditVar(idx){\n if(!currentConfig.models||!currentConfig.models[idx]) return;\n _editVarIdx = idx;\n var m = currentConfig.models[idx];\n document.getElementById('editVarName').value = m.name||'';\n document.getElementById('editVarEnvVar').value = m.useEnvVar||'';\n document.getElementById('editVarApiKey').value = m.apiKey||'';\n document.getElementById('editVarForm').style.display='';\n}\nfunction finishEditVar(){\n if(_editVarIdx<0 || !currentConfig.models||!currentConfig.models[_editVarIdx]) return;\n var name = document.getElementById('editVarName').value.trim();\n var useEnvVar = document.getElementById('editVarEnvVar').value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'').trim();\n var apiKey = document.getElementById('editVarApiKey').value.trim();\n if(!name){ toast('Display Name required','err'); return; }\n if(!useEnvVar){ toast('Env Var required','err'); return; }\n currentConfig.models[_editVarIdx] = {id:useEnvVar, name:name, types:['env-var'], apiKey:apiKey, baseURL:'', useEnvVar:useEnvVar};\n _editVarIdx = -1;\n renderVarsTable();\n saveConfig();\n}\nfunction cancelEditVar(){\n _editVarIdx = -1;\n document.getElementById('editVarForm').style.display='none';\n}\nfunction confirmDeleteVar(idx){\n _deleteVarIdx = idx;\n var models = (currentConfig&&currentConfig.models)||[];\n var name = models[idx] ? models[idx].name : 'this var';\n document.getElementById('varDeleteName').textContent = name;\n document.getElementById('varDeleteModal').classList.add('open');\n}\nfunction closeVarDeleteModal(){\n document.getElementById('varDeleteModal').classList.remove('open');\n _deleteVarIdx = -1;\n}\nfunction doDeleteVar(){\n if(_deleteVarIdx>=0 && currentConfig.models){\n currentConfig.models.splice(_deleteVarIdx,1);\n renderVarsTable();\n saveConfig();\n }\n closeVarDeleteModal();\n}\n\n/* ---- Internal Tools discovery ---- */\nvar _internalToolsLoaded = false;\nasync function openInternalToolsModal(){\n document.getElementById('internalToolsModal').classList.add('open');\n if(_internalToolsLoaded) return;\n try {\n var data = await fetchAPI('/internal-tools');\n var wrap = document.getElementById('internalToolsBody');\n if(!data || !data.length){ wrap.innerHTML = '<p style=\"color:var(--text-muted)\">No internal tool servers registered.</p>'; _internalToolsLoaded = true; return; }\n var html = '<table class=\"tbl\" style=\"font-size:13px\"><thead><tr><th style=\"width:18%\">Server</th><th style=\"width:20%\">Tool</th><th>Parameters</th></tr></thead><tbody>';\n for(var s = 0; s < data.length; s++){\n var srv = data[s];\n var toolCount = srv.tools.length || 1;\n for(var t = 0; t < srv.tools.length; t++){\n var tl = srv.tools[t];\n html += '<tr>';\n if(t === 0) html += '<td rowspan=\"'+toolCount+'\" style=\"vertical-align:top\"><strong>'+esc(srv.server)+'</strong></td>';\n html += '<td style=\"vertical-align:top\"><code>'+esc(tl.name)+'</code><div style=\"color:var(--text-muted);font-size:11px;margin-top:4px\">'+esc(tl.description)+'</div></td>';\n if(tl.params.length === 0){\n html += '<td style=\"color:var(--text-muted);font-style:italic\">none</td>';\n } else {\n html += '<td>';\n for(var p = 0; p < tl.params.length; p++){\n var pm = tl.params[p];\n if(p > 0) html += '<br>';\n html += '<code>'+esc(pm.name)+'</code> <span style=\"color:var(--text-muted)\">('+esc(pm.type)+(pm.required?'':',opt')+')</span>';\n if(pm.description) html += ' — <span style=\"font-size:12px\">'+esc(pm.description)+'</span>';\n }\n html += '</td>';\n }\n html += '</tr>';\n }\n if(srv.tools.length === 0){\n html += '<tr><td><strong>'+esc(srv.server)+'</strong></td><td colspan=\"2\" style=\"color:var(--text-muted);font-style:italic\">No tools</td></tr>';\n }\n }\n html += '</tbody></table>';\n wrap.innerHTML = html;\n _internalToolsLoaded = true;\n } catch(err) {\n document.getElementById('internalToolsBody').innerHTML = '<p style=\"color:var(--danger)\">Failed to load: '+esc(String(err))+'</p>';\n }\n}\n"}
1
+ export function agentJS(){return"\nvar SA_TOOL_LIST = ['Read','Write','Edit','Bash','Glob','Grep','WebSearch','WebFetch'];\n\nvar _editModelIdx = -1;\n\n/* ---- Models ---- */\nfunction showAddModel(){\n document.getElementById('addModelForm').style.display='';\n // Reset form\n document.getElementById('newModelId').value='';\n document.getElementById('newModelName').value='';\n document.getElementById('newModelBaseURL').value='https://api.openai.com/v1';\n document.getElementById('newModelApiKey').value='';\n document.getElementById('newModelEnvVar').value='';\n document.getElementById('newModelType').value='external';\n document.getElementById('newModelProxy').value='not-used';\n document.getElementById('newModelFastUrl').value='';\n document.getElementById('newModelFastProxyApiKey').value='';\n document.getElementById('newModelContextWindow').value='200000';\n document.getElementById('newModelCostInput').value='0';\n document.getElementById('newModelCostOutput').value='0';\n document.getElementById('newModelCostCacheRead').value='0';\n document.getElementById('newModelCostCacheWrite').value='0';\n updateNewModelApiFields();\n}\nfunction hideAddModel(){ document.getElementById('addModelForm').style.display='none'; }\nfunction sanitizeEnvVarInput(el){\n var v = el.value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'');\n if(v !== el.value) el.value = v;\n}\nfunction updateNewModelApiFields(){\n var type = document.getElementById('newModelType').value;\n document.getElementById('newModelApiFields').style.display = type!=='internal' ? '' : 'none';\n var baseField = document.getElementById('newModelBaseURLField');\n if(baseField) baseField.style.display = type==='external' ? '' : 'none';\n var proxyField = document.getElementById('newModelProxyField');\n if(proxyField) proxyField.style.display = type==='external' ? '' : 'none';\n var extraFields = document.getElementById('newModelExtraFields');\n if(extraFields) extraFields.style.display = type==='external' ? '' : 'none';\n updateNewModelProxyFields();\n}\nfunction updateNewModelProxyFields(){\n var proxy = document.getElementById('newModelProxy').value;\n var enabled = proxy !== 'not-used';\n var fu = document.getElementById('newModelFastUrl');\n var fk = document.getElementById('newModelFastProxyApiKey');\n if(fu) fu.disabled = !enabled;\n if(fk) fk.disabled = !enabled;\n}\nfunction updateEditModelApiFields(){\n var type = document.getElementById('editModelType').value;\n document.getElementById('editModelApiFields').style.display = type!=='internal' ? '' : 'none';\n var baseField = document.getElementById('editModelBaseURLField');\n if(baseField) baseField.style.display = type==='external' ? '' : 'none';\n var proxyField = document.getElementById('editModelProxyField');\n if(proxyField) proxyField.style.display = type==='external' ? '' : 'none';\n var extraFields = document.getElementById('editModelExtraFields');\n if(extraFields) extraFields.style.display = type==='external' ? '' : 'none';\n updateEditModelProxyFields();\n}\nfunction updateEditModelProxyFields(){\n var proxy = document.getElementById('editModelProxy').value;\n var enabled = proxy !== 'not-used';\n var fu = document.getElementById('editModelFastUrl');\n var fk = document.getElementById('editModelFastProxyApiKey');\n if(fu) fu.disabled = !enabled;\n if(fk) fk.disabled = !enabled;\n}\nasync function loadModels(){\n currentConfig = await fetchAPI('/config');\n if(!currentConfig.models) currentConfig.models=[];\n renderModelsTable();\n}\nfunction renderModelsTable(){\n var models = currentConfig.models||[];\n var tbody = document.getElementById('modelsBody');\n tbody.innerHTML='';\n var hasVisible = false;\n for(var i=0;i<models.length;i++){\n var m = models[i];\n var types = m.types||['external'];\n if(types.indexOf('env-var')!==-1) continue;\n hasVisible = true;\n var typeBadges = types.map(function(t){ return '<span class=\"badge badge-blue\" style=\"font-size:11px;padding:1px 6px;margin-right:2px\">'+esc(t)+'</span>'; }).join('');\n var proxyBadge = (m.proxy && m.proxy !== 'not-used') ? ' <span class=\"badge badge-green\" style=\"font-size:11px;padding:1px 6px\">'+esc(m.proxy)+'</span>' : '';\n tbody.innerHTML += '<tr data-model-idx=\"'+i+'\"><td>'+esc(m.name)+'</td><td style=\"font-family:monospace;font-size:13px\">'+esc(m.id)+'</td><td>'+typeBadges+proxyBadge+'</td><td style=\"white-space:nowrap\"><button class=\"btn-ghost btn-sm\" onclick=\"startEditModel('+i+')\">Edit</button> <button class=\"btn-danger btn-sm\" onclick=\"confirmDeleteModel('+i+')\">Delete</button></td></tr>';\n }\n if(!hasVisible){\n tbody.innerHTML='<tr><td colspan=\"4\" style=\"text-align:center;color:var(--text-muted);padding:20px\">No models in registry</td></tr>';\n }\n // Hide edit form when re-rendering\n document.getElementById('editModelForm').style.display='none';\n _editModelIdx = -1;\n}\nfunction addModel(){\n var id = document.getElementById('newModelId').value.trim();\n var name = document.getElementById('newModelName').value.trim();\n if(!id){ toast('Model ID required','err'); return; }\n if(!name) name = id;\n var type = document.getElementById('newModelType').value;\n var types = [type];\n var needsApi = type!=='internal';\n var useEnvVar = needsApi ? document.getElementById('newModelEnvVar').value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'').trim() : '';\n var apiKey = needsApi ? document.getElementById('newModelApiKey').value.trim() : '';\n var baseURL = type==='external' ? document.getElementById('newModelBaseURL').value.trim() : '';\n var proxy = type==='external' ? document.getElementById('newModelProxy').value : 'not-used';\n var fastUrl = (type==='external' && proxy!=='not-used') ? document.getElementById('newModelFastUrl').value.trim() : '';\n var fastProxyApiKey = (type==='external' && proxy!=='not-used') ? document.getElementById('newModelFastProxyApiKey').value.trim() : '';\n var contextWindow = type==='external' ? (parseInt(document.getElementById('newModelContextWindow').value)||200000) : 200000;\n var costInput = type==='external' ? (parseFloat(document.getElementById('newModelCostInput').value)||0) : 0;\n var costOutput = type==='external' ? (parseFloat(document.getElementById('newModelCostOutput').value)||0) : 0;\n var costCacheRead = type==='external' ? (parseFloat(document.getElementById('newModelCostCacheRead').value)||0) : 0;\n var costCacheWrite = type==='external' ? (parseFloat(document.getElementById('newModelCostCacheWrite').value)||0) : 0;\n if(!currentConfig.models) currentConfig.models=[];\n var dup = currentConfig.models.some(function(m){ return m.name===name && m.id===id; });\n if(dup){ toast('A model configuration with the same Model Name and Model ID already exists','err'); return; }\n currentConfig.models.push({id:id, name:name, types:types, proxy:proxy, fastUrl:fastUrl, fastProxyApiKey:fastProxyApiKey, apiKey:apiKey, baseURL:baseURL, useEnvVar:useEnvVar, contextWindow:contextWindow, costInput:costInput, costOutput:costOutput, costCacheRead:costCacheRead, costCacheWrite:costCacheWrite});\n hideAddModel();\n renderModelsTable();\n populateModelSelects();\n saveConfig();\n}\nvar _deleteModelIdx = -1;\nfunction confirmDeleteModel(idx){\n _deleteModelIdx = idx;\n var models = (currentConfig&&currentConfig.models)||[];\n var name = models[idx] ? models[idx].name : 'this model';\n document.getElementById('modelDeleteName').textContent = name;\n document.getElementById('modelDeleteModal').classList.add('open');\n}\nfunction closeModelDeleteModal(){\n document.getElementById('modelDeleteModal').classList.remove('open');\n _deleteModelIdx = -1;\n}\nfunction doDeleteModel(){\n if(_deleteModelIdx>=0 && currentConfig.models){\n currentConfig.models.splice(_deleteModelIdx,1);\n renderModelsTable();\n populateModelSelects();\n saveConfig();\n }\n closeModelDeleteModal();\n}\nfunction startEditModel(idx){\n if(!currentConfig.models||!currentConfig.models[idx]) return;\n _editModelIdx = idx;\n var m = currentConfig.models[idx];\n document.getElementById('editModelId').value = m.id||'';\n document.getElementById('editModelName').value = m.name||'';\n document.getElementById('editModelBaseURL').value = m.baseURL||'';\n document.getElementById('editModelApiKey').value = m.apiKey||'';\n document.getElementById('editModelEnvVar').value = m.useEnvVar||'';\n document.getElementById('editModelProxy').value = m.proxy||'not-used';\n document.getElementById('editModelFastUrl').value = m.fastUrl||'';\n document.getElementById('editModelFastProxyApiKey').value = m.fastProxyApiKey||'';\n document.getElementById('editModelContextWindow').value = m.contextWindow||200000;\n document.getElementById('editModelCostInput').value = m.costInput||0;\n document.getElementById('editModelCostOutput').value = m.costOutput||0;\n document.getElementById('editModelCostCacheRead').value = m.costCacheRead||0;\n document.getElementById('editModelCostCacheWrite').value = m.costCacheWrite||0;\n var types = m.types||['external'];\n document.getElementById('editModelType').value = types[0]||'external';\n updateEditModelApiFields();\n // Position edit form after the table\n document.getElementById('editModelForm').style.display='';\n}\nfunction finishEditModel(){\n if(_editModelIdx<0 || !currentConfig.models||!currentConfig.models[_editModelIdx]) return;\n var id = document.getElementById('editModelId').value.trim();\n var name = document.getElementById('editModelName').value.trim();\n if(!id){ toast('Model ID required','err'); return; }\n if(!name) name = id;\n var type = document.getElementById('editModelType').value;\n var types = [type];\n var needsApi = type!=='internal';\n var useEnvVar = needsApi ? document.getElementById('editModelEnvVar').value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'').trim() : '';\n var apiKey = needsApi ? document.getElementById('editModelApiKey').value.trim() : '';\n var baseURL = type==='external' ? document.getElementById('editModelBaseURL').value.trim() : '';\n var proxy = type==='external' ? document.getElementById('editModelProxy').value : 'not-used';\n var fastUrl = (type==='external' && proxy!=='not-used') ? document.getElementById('editModelFastUrl').value.trim() : '';\n var fastProxyApiKey = (type==='external' && proxy!=='not-used') ? document.getElementById('editModelFastProxyApiKey').value.trim() : '';\n var contextWindow = type==='external' ? (parseInt(document.getElementById('editModelContextWindow').value)||200000) : 200000;\n var costInput = type==='external' ? (parseFloat(document.getElementById('editModelCostInput').value)||0) : 0;\n var costOutput = type==='external' ? (parseFloat(document.getElementById('editModelCostOutput').value)||0) : 0;\n var costCacheRead = type==='external' ? (parseFloat(document.getElementById('editModelCostCacheRead').value)||0) : 0;\n var costCacheWrite = type==='external' ? (parseFloat(document.getElementById('editModelCostCacheWrite').value)||0) : 0;\n var dup = currentConfig.models.some(function(m, i){ return i!==_editModelIdx && m.name===name && m.id===id; });\n if(dup){ toast('A model configuration with the same Model Name and Model ID already exists','err'); return; }\n currentConfig.models[_editModelIdx] = {id:id, name:name, types:types, proxy:proxy, fastUrl:fastUrl, fastProxyApiKey:fastProxyApiKey, apiKey:apiKey, baseURL:baseURL, useEnvVar:useEnvVar, contextWindow:contextWindow, costInput:costInput, costOutput:costOutput, costCacheRead:costCacheRead, costCacheWrite:costCacheWrite};\n _editModelIdx = -1;\n renderModelsTable();\n populateModelSelects();\n saveConfig();\n}\nfunction cancelEditModel(){\n _editModelIdx = -1;\n document.getElementById('editModelForm').style.display='none';\n}\nfunction populateModelSelects(){\n var models = (currentConfig&&currentConfig.models)||[];\n var picoEnabled = document.getElementById('picoEnabled') && document.getElementById('picoEnabled').checked;\n var picoNames = {};\n if (picoEnabled) { for (var pi=0; pi<_picoModels.length; pi++) picoNames[_picoModels[pi].name] = true; }\n var internalModels = models.filter(function(m){ var t=m.types||['external']; return t.indexOf('internal')!==-1 || (t.indexOf('external')!==-1 && m.proxy && m.proxy!=='not-used') || (picoEnabled && t.indexOf('external')!==-1 && picoNames[m.name]); });\n var selects = {\n agentModel: {val:'', allowNone:false},\n agentMainFallback: {val:'', allowNone:true}\n };\n for(var key in selects){\n var el = document.getElementById(key);\n if(!el) continue;\n selects[key].val = el.value;\n el.innerHTML='';\n if(selects[key].allowNone) el.innerHTML += '<option value=\"\">None</option>';\n for(var i=0;i<internalModels.length;i++){\n el.innerHTML += '<option value=\"'+esc(internalModels[i].name+':'+internalModels[i].id)+'\">'+esc(internalModels[i].name)+' ('+esc(internalModels[i].id)+')</option>';\n }\n el.value = selects[key].val;\n }\n populateSTTModelSelect();\n populateMemSearchModelSelect();\n populatePicoModelSelect();\n populateRollingModelSelect();\n}\nfunction populateSTTModelSelect(){\n var models = (currentConfig&&currentConfig.models)||[];\n var el = document.getElementById('sttModelRef');\n if(!el) return;\n var prev = el.value;\n el.innerHTML = '<option value=\"\">-- select --</option>';\n for(var i=0;i<models.length;i++){\n var types = models[i].types||['external'];\n if(types.indexOf('external')===-1) continue;\n el.innerHTML += '<option value=\"'+esc(models[i].name+':'+models[i].id)+'\">'+esc(models[i].name)+' ('+esc(models[i].id)+')</option>';\n }\n el.value = prev;\n}\nfunction populateMemSearchModelSelect(){\n var models = (currentConfig&&currentConfig.models)||[];\n var el = document.getElementById('memSearchModelRef');\n if(!el) return;\n var prev = el.value;\n el.innerHTML = '<option value=\"\">-- select --</option>';\n for(var i=0;i<models.length;i++){\n var types = models[i].types||['external'];\n if(types.indexOf('external')===-1) continue;\n el.innerHTML += '<option value=\"'+esc(models[i].name+':'+models[i].id)+'\">'+esc(models[i].name)+' ('+esc(models[i].id)+')</option>';\n }\n el.value = prev;\n}\n\n/* ---- Pico Agent ---- */\nvar _picoModels = []; // [{name, piProvider, piModelId, contextWindow}]\nvar _agentLoading = false; // suppress markDirty during initial load\n\nfunction detectPiProvider(model) {\n var url = (model.baseURL || '').toLowerCase();\n if (url.includes('openrouter.ai')) return 'openrouter';\n if (url.includes('openai.com')) return 'openai';\n if (url.includes('x.ai')) return 'xai';\n if (url.includes('googleapis.com')) return 'google';\n if (url.includes('groq.com')) return 'groq';\n if (url.includes('mistral.ai')) return 'mistral';\n return 'openai';\n}\n\nfunction loadPicoAgent() {\n var a = (currentConfig && currentConfig.agent) || {};\n var pico = a.picoAgent || {};\n // Fallback: migrate from engine if picoAgent absent and engine.type === \"pi\"\n if (!a.picoAgent && a.engine && a.engine.type === 'pi') {\n pico = { enabled: true, modelRefs: [], rollingMemoryModel: '' };\n if (a.engine.piModelRef) {\n pico.modelRefs = [a.engine.piModelRef];\n }\n }\n var el = document.getElementById('picoEnabled');\n if (el) el.checked = !!pico.enabled;\n // Parse modelRefs into _picoModels\n _picoModels = [];\n var refs = pico.modelRefs || [];\n var models = (currentConfig && currentConfig.models) || [];\n for (var i = 0; i < refs.length; i++) {\n var parts = refs[i].split(':');\n var name = parts[0] || '';\n var piProvider = parts.length >= 3 ? parts[1] : '';\n var piModelId = parts.length >= 3 ? parts.slice(2).join(':') : (parts[1] || '');\n // Find matching model entry for contextWindow\n var matched = models.filter(function(m){ return m.name === name; })[0];\n var ctx = (matched && matched.contextWindow) || 200000;\n if (!piProvider && matched) piProvider = detectPiProvider(matched);\n _picoModels.push({ name: name, piProvider: piProvider, piModelId: piModelId, contextWindow: ctx });\n }\n updatePicoFields();\n renderPicoModelList();\n // Set rolling model AFTER populateRollingModelSelect() has built the options\n var rollingEl = document.getElementById('picoRollingModel');\n if (rollingEl) rollingEl.value = pico.rollingMemoryModel || '';\n}\n\nfunction updatePicoFields() {\n var enabled = document.getElementById('picoEnabled').checked;\n var fields = document.getElementById('picoFields');\n if (fields) fields.style.display = enabled ? '' : 'none';\n if (enabled) {\n populatePicoModelSelect();\n populateRollingModelSelect();\n }\n populateModelSelects();\n if (!_agentLoading) markDirty();\n}\n\nfunction populatePicoModelSelect() {\n var models = (currentConfig && currentConfig.models) || [];\n var el = document.getElementById('picoModelSelect');\n if (!el) return;\n el.innerHTML = '<option value=\"\" disabled selected>Select a model...</option>';\n var alreadyAdded = {};\n for (var j = 0; j < _picoModels.length; j++) alreadyAdded[_picoModels[j].name] = true;\n var hasOptions = false;\n for (var i = 0; i < models.length; i++) {\n var types = models[i].types || ['external'];\n if (types.indexOf('external') === -1) continue;\n if (alreadyAdded[models[i].name]) continue;\n el.innerHTML += '<option value=\"' + esc(models[i].name) + '\">' + esc(models[i].name) + ' (' + esc(models[i].id) + ')</option>';\n hasOptions = true;\n }\n if (!hasOptions) {\n el.innerHTML = '<option value=\"\" disabled selected>-- no available models --</option>';\n }\n}\n\nfunction populateRollingModelSelect() {\n var el = document.getElementById('picoRollingModel');\n if (!el) return;\n var prev = el.value;\n el.innerHTML = '<option value=\"\">-- none --</option>';\n // Populate from _picoModels so values are full modelRefs (Name:provider:modelId)\n for (var i = 0; i < _picoModels.length; i++) {\n var m = _picoModels[i];\n var ref = m.name + ':' + m.piProvider + ':' + m.piModelId;\n el.innerHTML += '<option value=\"' + esc(ref) + '\">' + esc(m.name) + ' (' + esc(m.piProvider + ':' + m.piModelId) + ')</option>';\n }\n el.value = prev;\n}\n\nfunction addPicoModel() {\n var sel = document.getElementById('picoModelSelect');\n var name = sel.value;\n if (!name) return;\n var models = (currentConfig && currentConfig.models) || [];\n var matched = models.filter(function(m){ return m.name === name; })[0];\n if (!matched) return;\n var piProvider = detectPiProvider(matched);\n var piModelId = matched.id || '';\n // Strip registry namespace prefix (e.g. \"openrouter:openai/gpt-5.2\" → \"openai/gpt-5.2\")\n var nsIdx = piModelId.indexOf(':');\n if (nsIdx > 0) piModelId = piModelId.substring(nsIdx + 1);\n var ctx = matched.contextWindow || 200000;\n _picoModels.push({ name: name, piProvider: piProvider, piModelId: piModelId, contextWindow: ctx });\n renderPicoModelList();\n populatePicoModelSelect();\n populateModelSelects();\n markDirty();\n}\n\nfunction removePicoModel(idx) {\n _picoModels.splice(idx, 1);\n renderPicoModelList();\n populatePicoModelSelect();\n populateModelSelects();\n markDirty();\n}\n\nfunction clearRollingModel() {\n var el = document.getElementById('picoRollingModel');\n if (el) el.value = '';\n markDirty();\n}\n\nfunction renderPicoModelList() {\n var container = document.getElementById('picoModelList');\n if (!container) return;\n if (_picoModels.length === 0) {\n container.innerHTML = '<div style=\"color:var(--text-muted);font-size:13px;padding:8px 0\">No models added. Use the select above to add models.</div>';\n return;\n }\n var html = '';\n for (var i = 0; i < _picoModels.length; i++) {\n var m = _picoModels[i];\n var ctxLabel = m.contextWindow >= 1000 ? Math.round(m.contextWindow / 1000) + 'k ctx' : m.contextWindow + ' ctx';\n html += '<div class=\"pico-model-item\" draggable=\"true\" data-pico-idx=\"' + i + '\">';\n html += '<span class=\"pico-drag-handle\">⠇</span>';\n html += '<span class=\"pico-model-name\">' + esc(m.name) + '</span>';\n html += '<span class=\"pico-model-ctx\">' + esc(ctxLabel) + '</span>';\n if (i === 0) html += '<span class=\"badge badge-blue\" style=\"font-size:11px;padding:1px 6px\">default</span>';\n html += '<button class=\"pico-model-remove\" onclick=\"removePicoModel(' + i + ')\">&times;</button>';\n html += '</div>';\n }\n container.innerHTML = html;\n // Attach drag events\n var items = container.querySelectorAll('.pico-model-item');\n items.forEach(function(item) {\n item.addEventListener('dragstart', function(e) {\n e.dataTransfer.setData('text/plain', item.dataset.picoIdx);\n item.classList.add('dragging');\n });\n item.addEventListener('dragend', function() {\n item.classList.remove('dragging');\n });\n item.addEventListener('dragover', function(e) {\n e.preventDefault();\n });\n item.addEventListener('drop', function(e) {\n e.preventDefault();\n var fromIdx = parseInt(e.dataTransfer.getData('text/plain'));\n var toIdx = parseInt(item.dataset.picoIdx);\n if (fromIdx === toIdx) return;\n var moved = _picoModels.splice(fromIdx, 1)[0];\n _picoModels.splice(toIdx, 0, moved);\n renderPicoModelList();\n markDirty();\n });\n });\n}\n\n/* ---- Model ref upgrade ---- */\nfunction upgradeModelRef(val, models){\n if(!val) return val;\n if(val.indexOf(':')!==-1) return val;\n var m = models.filter(function(x){ return x.name===val; })[0];\n return m ? m.name+':'+m.id : val;\n}\n\n/* ---- Agent ---- */\nasync function loadAgent(){\n currentConfig = await fetchAPI('/config');\n const a = currentConfig.agent||{};\n var models = currentConfig.models||[];\n // Load pico agent FIRST so _picoModels is populated before populateModelSelects()\n _agentLoading = true;\n loadPicoAgent();\n populateModelSelects();\n document.getElementById('agentModel').value = upgradeModelRef(a.model||'', models);\n document.getElementById('agentMainFallback').value = upgradeModelRef(a.mainFallback||'', models);\n document.getElementById('agentMaxTurns').value = a.maxTurns||10;\n document.getElementById('agentPermMode').value = a.permissionMode||'default';\n document.getElementById('agentSessionTTL').value = a.sessionTTL||3600;\n document.getElementById('agentSettingSources').value = a.settingSources||'project';\n document.getElementById('agentCoderSkill').checked = !!a.builtinCoderSkill;\n document.getElementById('agentAutoRenew').value = a.autoRenew||0;\n var allowed = a.allowedTools||[];\n document.querySelectorAll('#agentToolsGrid [data-tool]').forEach(function(cb){cb.checked = allowed.indexOf(cb.dataset.tool)!==-1;});\n document.getElementById('agentQueueMode').value = a.queueMode||'collect';\n document.getElementById('agentDebounceMs').value = a.queueDebounceMs!=null ? a.queueDebounceMs : 1500;\n document.getElementById('agentQueueCap').value = a.queueCap!=null ? a.queueCap : 20;\n document.getElementById('agentDropPolicy').value = a.queueDropPolicy||'summarize';\n document.getElementById('agentInflightTyping').checked = a.inflightTyping!==false;\n document.getElementById('agentAutoApprove').checked = a.autoApproveTools!==false;\n updateQueueFields();\n _agentLoading = false;\n sectionsLoaded.agent = true;\n}\n\n/* ---- Queue fields visibility ---- */\nfunction updateQueueFields(){\n var mode = document.getElementById('agentQueueMode').value;\n document.getElementById('queueCollectFields').style.display = mode==='collect' ? '' : 'none';\n}\n\n/* ---- SubAgents ---- */\nvar _saDeleteIdx = -1;\nasync function loadSubAgents(){\n currentConfig = await fetchAPI('/config');\n renderSubAgentCards();\n}\nfunction toggleSaAccordion(idx){\n var el = document.querySelector('.sa-acc[data-sa-idx=\"'+idx+'\"]');\n if(el) el.classList.toggle('open');\n}\nfunction renderSubAgentCards(){\n var sas = (currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents) || [];\n var container = document.getElementById('subAgentCards');\n var empty = document.getElementById('subAgentEmpty');\n if(sas.length === 0){\n container.innerHTML = '';\n empty.style.display = '';\n return;\n }\n empty.style.display = 'none';\n var order = [];\n for(var k=0;k<sas.length;k++) order.push(k);\n order.sort(function(a,b){ return (sas[a].name||'').localeCompare(sas[b].name||''); });\n var html = '';\n for(var oi = 0; oi < order.length; oi++){\n var i = order[oi];\n var sa = sas[i];\n html += '<div class=\"sa-acc\" data-sa-idx=\"'+i+'\">';\n html += '<div class=\"sa-acc-header\" onclick=\"toggleSaAccordion('+i+')\">';\n html += '<svg class=\"sa-acc-chevron\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"6 9 12 15 18 9\"/></svg>';\n html += '<span class=\"sa-acc-name\">' + esc(sa.name) + '</span>';\n if(sa.expandContext) html += '<span class=\"badge badge-blue\" style=\"font-size:10px;padding:1px 6px\">expanded</span>';\n html += '<label class=\"toggle\" onclick=\"event.stopPropagation()\"><input type=\"checkbox\" data-sa-toggle=\"'+i+'\" '+(sa.enabled?'checked':'')+'><span></span></label>';\n html += '<button class=\"btn-danger btn-sm\" onclick=\"event.stopPropagation();confirmDeleteSubAgent('+i+')\">Delete</button>';\n html += '</div>';\n html += '<div class=\"sa-acc-body\">';\n html += '<div class=\"field\"><label>Description</label><textarea data-sa-field=\"'+i+'.description\" rows=\"2\" oninput=\"updateSaField('+i+',&quot;description&quot;,this.value)\">'+esc(sa.description)+'</textarea></div>';\n html += '<div class=\"field\"><label>Prompt</label><textarea data-sa-field=\"'+i+'.prompt\" rows=\"3\" oninput=\"updateSaField('+i+',&quot;prompt&quot;,this.value)\">'+esc(sa.prompt)+'</textarea></div>';\n html += '<div class=\"field\"><label>Model</label><select data-sa-field=\"'+i+'.model\" onchange=\"updateSaField('+i+',&quot;model&quot;,this.value)\">';\n var saModels = ['inherit','sonnet','opus','haiku'];\n for(var j=0;j<saModels.length;j++){\n html += '<option value=\"'+saModels[j]+'\"'+(sa.model===saModels[j]?' selected':'')+'>'+saModels[j]+'</option>';\n }\n html += '</select></div>';\n var saTools = sa.tools||[];\n html += '<div class=\"field\"><label>Tools</label><div style=\"display:flex;flex-wrap:wrap;gap:4px;margin-top:4px\">';\n for(var t=0;t<SA_TOOL_LIST.length;t++){\n var tn = SA_TOOL_LIST[t];\n var checked = saTools.indexOf(tn)!==-1;\n html += '<label class=\"tool-toggle-sm\"><label class=\"toggle-sm\"><input type=\"checkbox\" data-sa-tool=\"'+i+'\" data-tool-name=\"'+tn+'\"'+(checked?' checked':'')+'><span></span></label> '+tn+'</label>';\n }\n html += '</div></div>';\n html += '<div style=\"display:flex;align-items:center;gap:12px;margin-top:8px\"><span style=\"font-size:13px;font-weight:500\">Expand Context</span><label class=\"toggle\"><input type=\"checkbox\" data-sa-expand=\"'+i+'\"'+(sa.expandContext?' checked':'')+'><span></span></label></div>';\n html += '</div></div>';\n }\n container.innerHTML = html;\n // Bind expandContext listeners\n container.querySelectorAll('[data-sa-expand]').forEach(function(cb){\n cb.addEventListener('change', function(){\n var idx = parseInt(cb.dataset.saExpand);\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents && currentConfig.agent.customSubAgents[idx]){\n currentConfig.agent.customSubAgents[idx].expandContext = cb.checked;\n markDirty();\n renderSubAgentCards();\n }\n });\n });\n // Bind toggle listeners\n container.querySelectorAll('[data-sa-toggle]').forEach(function(cb){\n cb.addEventListener('change', function(){\n var idx = parseInt(cb.dataset.saToggle);\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents){\n currentConfig.agent.customSubAgents[idx].enabled = cb.checked;\n markDirty();\n }\n });\n });\n // Bind tool toggle listeners\n container.querySelectorAll('[data-sa-tool]').forEach(function(cb){\n cb.addEventListener('change', function(){\n var idx = parseInt(cb.dataset.saTool);\n var toolName = cb.dataset.toolName;\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents && currentConfig.agent.customSubAgents[idx]){\n var tools = currentConfig.agent.customSubAgents[idx].tools || [];\n if(cb.checked){\n if(tools.indexOf(toolName)===-1) tools.push(toolName);\n } else {\n tools = tools.filter(function(t){return t!==toolName;});\n }\n currentConfig.agent.customSubAgents[idx].tools = tools;\n markDirty();\n }\n });\n });\n}\nfunction confirmDeleteSubAgent(idx){\n _saDeleteIdx = idx;\n var sas = (currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents) || [];\n var name = sas[idx] ? sas[idx].name : 'this subagent';\n document.getElementById('saDeleteName').textContent = name;\n document.getElementById('saDeleteModal').classList.add('open');\n}\nfunction closeSaDeleteModal(){\n document.getElementById('saDeleteModal').classList.remove('open');\n _saDeleteIdx = -1;\n}\nfunction doDeleteSubAgent(){\n if(_saDeleteIdx >= 0) deleteSubAgent(_saDeleteIdx);\n closeSaDeleteModal();\n saveConfig();\n}\nfunction updateSaField(idx, field, value){\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents && currentConfig.agent.customSubAgents[idx]){\n currentConfig.agent.customSubAgents[idx][field] = value;\n markDirty();\n }\n}\nfunction updateSaTools(idx, value){\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents && currentConfig.agent.customSubAgents[idx]){\n currentConfig.agent.customSubAgents[idx].tools = value.split(',').map(function(s){return s.trim()}).filter(Boolean);\n markDirty();\n }\n}\nfunction showAddSubAgent(){\n document.getElementById('addSubAgentForm').style.display = '';\n document.getElementById('newSaName').value = '';\n document.getElementById('newSaDesc').value = '';\n document.getElementById('newSaPrompt').value = '';\n document.getElementById('newSaModel').value = 'inherit';\n document.querySelectorAll('#newSaToolsGrid [data-new-sa-tool]').forEach(function(cb){cb.checked = cb.dataset.newSaTool!=='Bash';});\n document.getElementById('newSaExpandContext').checked = false;\n document.getElementById('newSaName').focus();\n}\nfunction hideAddSubAgent(){\n document.getElementById('addSubAgentForm').style.display = 'none';\n}\nasync function addSubAgent(){\n var name = document.getElementById('newSaName').value.trim();\n var desc = document.getElementById('newSaDesc').value.trim();\n var prompt = document.getElementById('newSaPrompt').value.trim();\n var model = document.getElementById('newSaModel').value;\n var tools = [];\n document.querySelectorAll('#newSaToolsGrid [data-new-sa-tool]').forEach(function(cb){if(cb.checked) tools.push(cb.dataset.newSaTool);});\n if(!name){ toast('Name is required','err'); return; }\n if(!/^[a-zA-Z0-9_\\-\\[\\]!]+$/.test(name)){ toast('Name may only contain a-z A-Z 0-9 - _ [ ] !','err'); return; }\n if(!desc || desc.length < 10){ toast('Description must be at least 10 characters','err'); return; }\n if(!prompt || prompt.length < 10){ toast('Prompt must be at least 10 characters','err'); return; }\n if(!currentConfig){ currentConfig = await fetchAPI('/config'); }\n if(!currentConfig.agent) currentConfig.agent = {};\n if(!currentConfig.agent.customSubAgents) currentConfig.agent.customSubAgents = [];\n var expandContext = document.getElementById('newSaExpandContext').checked;\n currentConfig.agent.customSubAgents.push({ name:name, description:desc, prompt:prompt, model:model, tools:tools, expandContext:expandContext, enabled:false });\n hideAddSubAgent();\n renderSubAgentCards();\n saveConfig();\n}\nfunction deleteSubAgent(idx){\n if(!currentConfig || !currentConfig.agent || !currentConfig.agent.customSubAgents) return;\n currentConfig.agent.customSubAgents.splice(idx, 1);\n renderSubAgentCards();\n markDirty();\n}\n\n/* ---- Vars ---- */\nvar _editVarIdx = -1;\nvar _deleteVarIdx = -1;\n\nfunction showAddVar(){\n document.getElementById('addVarForm').style.display='';\n document.getElementById('newVarName').value='';\n document.getElementById('newVarEnvVar').value='';\n document.getElementById('newVarApiKey').value='';\n}\nfunction hideAddVar(){ document.getElementById('addVarForm').style.display='none'; }\nfunction addVar(){\n var name = document.getElementById('newVarName').value.trim();\n var useEnvVar = document.getElementById('newVarEnvVar').value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'').trim();\n var apiKey = document.getElementById('newVarApiKey').value.trim();\n if(!name){ toast('Display Name required','err'); return; }\n if(!useEnvVar){ toast('Env Var required','err'); return; }\n if(!currentConfig.models) currentConfig.models=[];\n currentConfig.models.push({id:useEnvVar, name:name, types:['env-var'], apiKey:apiKey, baseURL:'', useEnvVar:useEnvVar});\n hideAddVar();\n renderVarsTable();\n saveConfig();\n}\nasync function loadVars(){\n currentConfig = await fetchAPI('/config');\n if(!currentConfig.models) currentConfig.models=[];\n renderVarsTable();\n}\nfunction renderVarsTable(){\n var models = currentConfig.models||[];\n var tbody = document.getElementById('varsBody');\n tbody.innerHTML='';\n var hasVisible = false;\n for(var i=0;i<models.length;i++){\n var m = models[i];\n var types = m.types||['external'];\n if(types.indexOf('env-var')===-1) continue;\n hasVisible = true;\n var envDisplay = m.useEnvVar||m.id||'';\n tbody.innerHTML += '<tr data-var-idx=\"'+i+'\"><td>'+esc(m.name)+'</td><td style=\"font-family:monospace;font-size:13px\">'+esc(envDisplay)+'</td><td style=\"white-space:nowrap\"><button class=\"btn-ghost btn-sm\" onclick=\"startEditVar('+i+')\">Edit</button> <button class=\"btn-danger btn-sm\" onclick=\"confirmDeleteVar('+i+')\">Delete</button></td></tr>';\n }\n if(!hasVisible){\n tbody.innerHTML='<tr><td colspan=\"3\" style=\"text-align:center;color:var(--text-muted);padding:20px\">No vars in registry</td></tr>';\n }\n document.getElementById('editVarForm').style.display='none';\n _editVarIdx = -1;\n}\nfunction startEditVar(idx){\n if(!currentConfig.models||!currentConfig.models[idx]) return;\n _editVarIdx = idx;\n var m = currentConfig.models[idx];\n document.getElementById('editVarName').value = m.name||'';\n document.getElementById('editVarEnvVar').value = m.useEnvVar||'';\n document.getElementById('editVarApiKey').value = m.apiKey||'';\n document.getElementById('editVarForm').style.display='';\n}\nfunction finishEditVar(){\n if(_editVarIdx<0 || !currentConfig.models||!currentConfig.models[_editVarIdx]) return;\n var name = document.getElementById('editVarName').value.trim();\n var useEnvVar = document.getElementById('editVarEnvVar').value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'').trim();\n var apiKey = document.getElementById('editVarApiKey').value.trim();\n if(!name){ toast('Display Name required','err'); return; }\n if(!useEnvVar){ toast('Env Var required','err'); return; }\n currentConfig.models[_editVarIdx] = {id:useEnvVar, name:name, types:['env-var'], apiKey:apiKey, baseURL:'', useEnvVar:useEnvVar};\n _editVarIdx = -1;\n renderVarsTable();\n saveConfig();\n}\nfunction cancelEditVar(){\n _editVarIdx = -1;\n document.getElementById('editVarForm').style.display='none';\n}\nfunction confirmDeleteVar(idx){\n _deleteVarIdx = idx;\n var models = (currentConfig&&currentConfig.models)||[];\n var name = models[idx] ? models[idx].name : 'this var';\n document.getElementById('varDeleteName').textContent = name;\n document.getElementById('varDeleteModal').classList.add('open');\n}\nfunction closeVarDeleteModal(){\n document.getElementById('varDeleteModal').classList.remove('open');\n _deleteVarIdx = -1;\n}\nfunction doDeleteVar(){\n if(_deleteVarIdx>=0 && currentConfig.models){\n currentConfig.models.splice(_deleteVarIdx,1);\n renderVarsTable();\n saveConfig();\n }\n closeVarDeleteModal();\n}\n\n/* ---- Internal Tools discovery ---- */\nvar _internalToolsLoaded = false;\nasync function openInternalToolsModal(){\n document.getElementById('internalToolsModal').classList.add('open');\n if(_internalToolsLoaded) return;\n try {\n var data = await fetchAPI('/internal-tools');\n var wrap = document.getElementById('internalToolsBody');\n if(!data || !data.length){ wrap.innerHTML = '<p style=\"color:var(--text-muted)\">No internal tool servers registered.</p>'; _internalToolsLoaded = true; return; }\n var html = '<table class=\"tbl\" style=\"font-size:13px\"><thead><tr><th style=\"width:18%\">Server</th><th style=\"width:20%\">Tool</th><th>Parameters</th></tr></thead><tbody>';\n for(var s = 0; s < data.length; s++){\n var srv = data[s];\n var toolCount = srv.tools.length || 1;\n for(var t = 0; t < srv.tools.length; t++){\n var tl = srv.tools[t];\n html += '<tr>';\n if(t === 0) html += '<td rowspan=\"'+toolCount+'\" style=\"vertical-align:top\"><strong>'+esc(srv.server)+'</strong></td>';\n html += '<td style=\"vertical-align:top\"><code>'+esc(tl.name)+'</code><div style=\"color:var(--text-muted);font-size:11px;margin-top:4px\">'+esc(tl.description)+'</div></td>';\n if(tl.params.length === 0){\n html += '<td style=\"color:var(--text-muted);font-style:italic\">none</td>';\n } else {\n html += '<td>';\n for(var p = 0; p < tl.params.length; p++){\n var pm = tl.params[p];\n if(p > 0) html += '<br>';\n html += '<code>'+esc(pm.name)+'</code> <span style=\"color:var(--text-muted)\">('+esc(pm.type)+(pm.required?'':',opt')+')</span>';\n if(pm.description) html += ' — <span style=\"font-size:12px\">'+esc(pm.description)+'</span>';\n }\n html += '</td>';\n }\n html += '</tr>';\n }\n if(srv.tools.length === 0){\n html += '<tr><td><strong>'+esc(srv.server)+'</strong></td><td colspan=\"2\" style=\"color:var(--text-muted);font-style:italic\">No tools</td></tr>';\n }\n }\n html += '</tbody></table>';\n wrap.innerHTML = html;\n _internalToolsLoaded = true;\n } catch(err) {\n document.getElementById('internalToolsBody').innerHTML = '<p style=\"color:var(--danger)\">Failed to load: '+esc(String(err))+'</p>';\n }\n}\n"}
@@ -1 +1 @@
1
- import{sdkUserToPiUser as e,piAssistantToSdk as t,extractTextFromSdkAssistant as o}from"./pi-message-adapter.js";import{ToolRegistry as s,BUILTIN_TOOL_DEFINITIONS as n}from"./pi-tool-adapter.js";import{executeBuiltinTool as r}from"./pi-tool-executor.js";import{createToolRegistryFromOptions as a}from"./pi-mcp-bridge.js";import{buildSkillsAndCommandsBlock as i}from"./pi-skill-loader.js";import{compactContext as c,applySummarization as l,DEFAULT_COMPACTION_CONFIG as d}from"./pi-context-compactor.js";import{randomUUID as u}from"node:crypto";let m=null;async function p(){if(!m)try{const e=await import("@mariozechner/pi-ai");m={stream:e.stream,complete:e.complete,getModel:e.getModel,getModels:e.getModels,getProviders:e.getProviders}}catch(e){throw new Error(`Failed to load @mariozechner/pi-ai. Install it with: npm install @mariozechner/pi-ai\n${e}`)}return m}const g=new Map;const h=setInterval(function(){const e=Date.now();for(const[t,o]of g)e-o.lastAccessTime>864e5&&g.delete(t)},6e5);"function"==typeof h.unref&&h.unref();export function piQuery(m,h,k){const{prompt:T,options:b}=m;let _=new AbortController,v=!1,P=!1,$=h.modelId,C=h.provider;const M=b.resume||u();let R=g.get(M);if(!R){let e="";if("string"==typeof b.systemPrompt)e=b.systemPrompt;else if(b.systemPrompt&&"object"==typeof b.systemPrompt){const t=b.systemPrompt.preset,o=b.systemPrompt.append||"";e="claude_code"===t?"You are an AI coding assistant. You have access to tools for reading, writing, and editing files, running shell commands, searching code, and more. Use these tools to help the user with their coding tasks.\n\nKey behaviors:\n- Read files before editing them\n- Use Grep and Glob to explore the codebase\n- Run tests after making changes\n- Be thorough but concise in explanations\n- When writing code, follow existing patterns and conventions in the codebase\n\n"+o:o}R={id:M,messages:[],systemPrompt:e,totalCostUsd:0,totalInputTokens:0,totalOutputTokens:0,totalTurns:0,startTime:Date.now(),lastAccessTime:Date.now(),lastCompactedIndex:0,compactionCount:0,rollingContext:""},g.set(M,R)}R.lastAccessTime=Date.now();const I=k??new s;if(k||(I.registerTools(n),b.mcpServers&&Object.keys(b.mcpServers).length>0&&a(b).then(e=>{for(const t of e.getTools())t.name.startsWith("mcp__")&&I.registerTool(t)}).catch(e=>{console.error(`[PiAgent] Failed to bridge MCP tools: ${e}`)})),b.canUseTool&&I.setPermissionChecker(b.canUseTool),!k){const e=b.mcpServers??{};I.setExecutor(async(t,o,s)=>{if(t.startsWith("mcp__")){const{executeMcpTool:n}=await import("./pi-mcp-bridge.js");return n(t,o,s,e)}const n=await r(t,s,b.cwd,M);return n||{content:[{type:"text",text:`Unknown tool: ${t}`}],isError:!0}})}async function*W(s){if(P)return;let n=R.systemPrompt;const r=i(b.cwd||"");if(r&&(n+="\n\n"+r),R.messages.length>0){const e=c(R.messages,n,{...d,contextWindowTokens:h.contextWindowTokens??128e3},R.lastCompactedIndex,R.rollingContext);if(e.phase>0&&(R.compactionCount++,console.log(`[pi-query] Context compacted (cycle #${R.compactionCount}): phase=${e.phase}, tokens ${e.estimatedTokensBefore}->${e.estimatedTokensAfter}, masked=${e.maskedResults}, compacted=${e.compactedResults}`)),e.summarized&&e.summarizationPrompt)try{const t=await p();if(t){const o={...d,contextWindowTokens:h.contextWindowTokens??128e3};let s=C,n=$;if(o.summarizationModel){const e=o.summarizationModel.split("/",2);2===e.length?(s=e[0],n=e[1]):n=e[0]}const r=t.getModel(s,n),a=t.stream(r,{systemPrompt:"You are a conversation summarizer. Be concise and structured.",messages:[{role:"user",content:e.summarizationPrompt,timestamp:Date.now()}]},{...h.apiKey?{apiKey:h.apiKey}:{},...h.headers?{headers:h.headers}:{},temperature:.3,maxTokens:1024});for await(const e of a);const i=await a.result(),c=i.content?.filter(e=>"text"===e.type).map(e=>e.text).join("\n");c&&(R.rollingContext=c,l(R.messages,c,d.keepLastNMessages),console.log(`[pi-query] Phase 3 rolling summary applied, messages reduced to ${R.messages.length}`))}}catch(e){console.warn(`[pi-query] Phase 3 summarization failed (non-fatal): ${e}`)}R.lastCompactedIndex=R.messages.length}const a=e(s);R.messages.push(a),R.lastAccessTime=Date.now();const u=b.maxTurns??25;let m=0,g="",k="end_turn",T={input:0,output:0,cacheRead:0,cacheWrite:0,totalTokens:0,cost:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0}};const M=Date.now();for(;m<u&&!P&&!v;){m++;const e=I.getFilteredTools(b.allowedTools,b.disallowedTools);let s,r;try{s=await p()}catch(e){return void(yield y(R,"error_during_execution",g,k,$,M,T,[`${e}`]))}try{r=s.getModel(C,$)}catch(e){return void(yield y(R,"error_during_execution",g,k,$,M,T,[`Model not found: ${C}/${$}. ${e}`]))}const a={systemPrompt:n,messages:R.messages.map(x),tools:e.length>0?e:void 0},i={signal:_.signal};let c;h.apiKey&&(i.apiKey=h.apiKey),void 0!==h.temperature&&(i.temperature=h.temperature),void 0!==h.maxTokens&&(i.maxTokens=h.maxTokens),h.headers&&(i.headers=h.headers),h.reasoning&&"off"!==h.reasoning&&(i.reasoning=h.reasoning);try{const e=s.stream(r,a,i);for await(const t of e)if(P||v||_.signal.aborted)break;if("function"!=typeof e.result)throw new Error("Stream did not return a .result() method — ensure @mariozechner/pi-ai is correctly installed");c=await e.result()}catch(e){return v||_.signal.aborted?(v=!1,void(yield y(R,"error_during_execution",g,k,$,M,T,["aborted"]))):void(yield y(R,"error_during_execution",g,k,$,M,T,[`${e}`]))}w(T,c.usage),k=f(c.stopReason);const l=t(c);yield l,R.messages.push(c);const d=o(l);if(d&&(g=d),"toolUse"!==c.stopReason)break;const u=c.content.filter(e=>"toolCall"===e.type);if(0===u.length)break;for(const e of u){if(P||v||_.signal.aborted)break;const t=await I.checkPermission(e.name,e.arguments);if("deny"===t.behavior){const o={role:"toolResult",toolCallId:e.id,toolName:e.name,content:[{type:"text",text:`Permission denied: ${t.message}`}],isError:!0,timestamp:Date.now()};R.messages.push(o);continue}const o="allow"===t.behavior&&t.updatedInput?t.updatedInput:e.arguments,s=Date.now();let n;try{n=await I.execute(e.name,e.id,o)}catch(e){n={content:[{type:"text",text:`Tool execution error: ${e}`}],isError:!0}}const r=(Date.now()-s)/1e3;yield{type:"tool_progress",tool_name:e.name,elapsed_time_seconds:r};const a={role:"toolResult",toolCallId:e.id,toolName:e.name,content:n.content,isError:n.isError,timestamp:Date.now()};R.messages.push(a)}if(P||v||_.signal.aborted)return v=!1,void(yield y(R,"error_during_execution",g,k,$,M,T,["aborted"]))}m>=u&&!P?yield y(R,"error_max_turns",g,k,$,M,T):(v=!1,yield y(R,"success",g,k,$,M,T))}const D=async function*(){yield{type:"system",subtype:"init",slash_commands:[],session_id:M};try{for await(const e of T){if(P)break;(v||_.signal.aborted)&&(v=!1,_=new AbortController);for await(const t of W(e)){if(P)break;yield t}}}catch(e){P||(yield{type:"result",subtype:"error_during_execution",session_id:M,result:"",errors:[`${e}`]})}}();return{[Symbol.asyncIterator]:()=>D,async interrupt(){v=!0,_.abort()},async setModel(e){if(e.includes("/")){const[t,o]=e.split("/",2);C=t,$=o}else $=e},close(){P=!0,_.abort(),g.delete(M)},async supportedModels(){try{const e=await p();if(!e)return[];const t=e.getProviders(),o=[];for(const s of t)try{const t=e.getModels(s);for(const e of t)o.push({id:`${s}/${e.id}`,name:e.name||e.id})}catch{}return o}catch{return[]}}}}function f(e){switch(e){case"stop":return"end_turn";case"length":return"max_tokens";case"toolUse":return"tool_use";case"error":return"error";case"aborted":return"aborted";default:return e}}function y(e,t,o,s,n,r,a,i){const c=Date.now()-r;return e.totalCostUsd+=a.cost.total,e.totalTurns++,{type:"result",subtype:t,session_id:e.id,result:"success"===t?o:void 0,stop_reason:"success"===t?s:null,total_cost_usd:a.cost.total,duration_ms:c,num_turns:1,modelUsage:{[n]:{inputTokens:a.input,outputTokens:a.output,cacheReadInputTokens:a.cacheRead,cacheCreationInputTokens:a.cacheWrite,costUSD:a.cost.total}},...i?{errors:i}:{}}}function w(e,t){e.input+=t.input,e.output+=t.output,e.cacheRead+=t.cacheRead,e.cacheWrite+=t.cacheWrite,e.totalTokens+=t.totalTokens,e.cost.input+=t.cost.input,e.cost.output+=t.cost.output,e.cost.cacheRead+=t.cost.cacheRead,e.cost.cacheWrite+=t.cost.cacheWrite,e.cost.total+=t.cost.total}function x(e){return e}
1
+ import{sdkUserToPiUser as e,piAssistantToSdk as t,extractTextFromSdkAssistant as o}from"./pi-message-adapter.js";import{ToolRegistry as s,BUILTIN_TOOL_DEFINITIONS as n}from"./pi-tool-adapter.js";import{executeBuiltinTool as r}from"./pi-tool-executor.js";import{createToolRegistryFromOptions as a}from"./pi-mcp-bridge.js";import{buildSkillsAndCommandsBlock as i}from"./pi-skill-loader.js";import{compactContext as c,applySummarization as l,DEFAULT_COMPACTION_CONFIG as d}from"./pi-context-compactor.js";import{randomUUID as u}from"node:crypto";let m=null;async function p(){if(!m)try{const e=await import("@mariozechner/pi-ai");m={stream:e.stream,complete:e.complete,getModel:e.getModel,getModels:e.getModels,getProviders:e.getProviders}}catch(e){throw new Error(`Failed to load @mariozechner/pi-ai. Install it with: npm install @mariozechner/pi-ai\n${e}`)}return m}const g=new Map;const h=setInterval(function(){const e=Date.now();for(const[t,o]of g)e-o.lastAccessTime>864e5&&g.delete(t)},6e5);"function"==typeof h.unref&&h.unref();export function piQuery(m,h,b){const{prompt:_,options:k}=m;let T=new AbortController,v=!1,P=!1,$=h.modelId,C=h.provider;const I=k.resume||u();let R=g.get(I);if(!R){let e="";if("string"==typeof k.systemPrompt)e=k.systemPrompt;else if(k.systemPrompt&&"object"==typeof k.systemPrompt){const t=k.systemPrompt.preset,o=k.systemPrompt.append||"";e="claude_code"===t?"You are an AI coding assistant. You have access to tools for reading, writing, and editing files, running shell commands, searching code, and more. Use these tools to help the user with their coding tasks.\n\nKey behaviors:\n- Read files before editing them\n- Use Grep and Glob to explore the codebase\n- Run tests after making changes\n- Be thorough but concise in explanations\n- When writing code, follow existing patterns and conventions in the codebase\n\n"+o:o}R={id:I,messages:[],systemPrompt:e,totalCostUsd:0,totalInputTokens:0,totalOutputTokens:0,totalTurns:0,startTime:Date.now(),lastAccessTime:Date.now(),lastCompactedIndex:0,compactionCount:0,rollingContext:""},g.set(I,R)}R.lastAccessTime=Date.now();const M=b??new s;if(b||(M.registerTools(n),k.mcpServers&&Object.keys(k.mcpServers).length>0&&a(k).then(e=>{for(const t of e.getTools())t.name.startsWith("mcp__")&&M.registerTool(t)}).catch(e=>{console.error(`[PiAgent] Failed to bridge MCP tools: ${e}`)})),k.canUseTool&&M.setPermissionChecker(k.canUseTool),!b){const e=k.mcpServers??{};M.setExecutor(async(t,o,s)=>{if(t.startsWith("mcp__")){const{executeMcpTool:n}=await import("./pi-mcp-bridge.js");return n(t,o,s,e)}const n=await r(t,s,k.cwd,I);return n||{content:[{type:"text",text:`Unknown tool: ${t}`}],isError:!0}})}async function*D(s){if(P)return;let n=R.systemPrompt;const r=i(k.cwd||"");if(r&&(n+="\n\n"+r),R.messages.length>0){const e=c(R.messages,n,{...d,contextWindowTokens:h.contextWindowTokens??128e3},R.lastCompactedIndex,R.rollingContext);if(e.phase>0&&(R.compactionCount++,console.log(`[pi-query] Context compacted (cycle #${R.compactionCount}): phase=${e.phase}, tokens ${e.estimatedTokensBefore}->${e.estimatedTokensAfter}, masked=${e.maskedResults}, compacted=${e.compactedResults}`)),e.summarized&&e.summarizationPrompt)try{const t=await p();if(t){const o=h.summarizationProvider||C,s=h.summarizationModelId||$,n=t.getModel(o,s),r=t.stream(n,{systemPrompt:"You are a conversation summarizer. Be concise and structured.",messages:[{role:"user",content:e.summarizationPrompt,timestamp:Date.now()}]},{...h.apiKey?{apiKey:h.apiKey}:{},...h.headers?{headers:h.headers}:{},temperature:.3,maxTokens:1024});for await(const e of r);const a=await r.result(),i=a.content?.filter(e=>"text"===e.type).map(e=>e.text).join("\n");i&&(R.rollingContext=i,l(R.messages,i,d.keepLastNMessages),console.log(`[pi-query] Phase 3 rolling summary applied, messages reduced to ${R.messages.length}`))}}catch(e){console.warn(`[pi-query] Phase 3 summarization failed (non-fatal): ${e}`)}R.lastCompactedIndex=R.messages.length}const a=e(s);R.messages.push(a),R.lastAccessTime=Date.now();const u=k.maxTurns??25;let m=0,g="",b="end_turn",_={input:0,output:0,cacheRead:0,cacheWrite:0,totalTokens:0,cost:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0}};const I=Date.now();for(;m<u&&!P&&!v;){m++;const e=M.getFilteredTools(k.allowedTools,k.disallowedTools);let s,r;try{s=await p()}catch(e){return void(yield y(R,"error_during_execution",g,b,$,I,_,[`${e}`]))}try{r=s.getModel(C,$)}catch(e){return void(yield y(R,"error_during_execution",g,b,$,I,_,[`Model not found: ${C}/${$}. ${e}`]))}const a={systemPrompt:n,messages:R.messages.map(x),tools:e.length>0?e:void 0},i={signal:T.signal};let c;h.apiKey&&(i.apiKey=h.apiKey),void 0!==h.temperature&&(i.temperature=h.temperature),void 0!==h.maxTokens&&(i.maxTokens=h.maxTokens),h.headers&&(i.headers=h.headers),h.reasoning&&"off"!==h.reasoning&&(i.reasoning=h.reasoning);try{const e=s.stream(r,a,i);for await(const t of e)if(P||v||T.signal.aborted)break;if("function"!=typeof e.result)throw new Error("Stream did not return a .result() method — ensure @mariozechner/pi-ai is correctly installed");c=await e.result()}catch(e){return v||T.signal.aborted?(v=!1,void(yield y(R,"error_during_execution",g,b,$,I,_,["aborted"]))):void(yield y(R,"error_during_execution",g,b,$,I,_,[`${e}`]))}w(_,c.usage),b=f(c.stopReason);const l=t(c);yield l,R.messages.push(c);const d=o(l);if(d&&(g=d),"toolUse"!==c.stopReason)break;const u=c.content.filter(e=>"toolCall"===e.type);if(0===u.length)break;for(const e of u){if(P||v||T.signal.aborted)break;const t=await M.checkPermission(e.name,e.arguments);if("deny"===t.behavior){const o={role:"toolResult",toolCallId:e.id,toolName:e.name,content:[{type:"text",text:`Permission denied: ${t.message}`}],isError:!0,timestamp:Date.now()};R.messages.push(o);continue}const o="allow"===t.behavior&&t.updatedInput?t.updatedInput:e.arguments,s=Date.now();let n;try{n=await M.execute(e.name,e.id,o)}catch(e){n={content:[{type:"text",text:`Tool execution error: ${e}`}],isError:!0}}const r=(Date.now()-s)/1e3;yield{type:"tool_progress",tool_name:e.name,elapsed_time_seconds:r};const a={role:"toolResult",toolCallId:e.id,toolName:e.name,content:n.content,isError:n.isError,timestamp:Date.now()};R.messages.push(a)}if(P||v||T.signal.aborted)return v=!1,void(yield y(R,"error_during_execution",g,b,$,I,_,["aborted"]))}m>=u&&!P?yield y(R,"error_max_turns",g,b,$,I,_):(v=!1,yield y(R,"success",g,b,$,I,_))}const W=async function*(){yield{type:"system",subtype:"init",slash_commands:[],session_id:I};try{for await(const e of _){if(P)break;(v||T.signal.aborted)&&(v=!1,T=new AbortController);for await(const t of D(e)){if(P)break;yield t}}}catch(e){P||(yield{type:"result",subtype:"error_during_execution",session_id:I,result:"",errors:[`${e}`]})}}();return{[Symbol.asyncIterator]:()=>W,async interrupt(){v=!0,T.abort()},async setModel(e){if(e.includes("/")){const[t,o]=e.split("/",2);C=t,$=o}else $=e},close(){P=!0,T.abort(),g.delete(I)},async supportedModels(){try{const e=await p();if(!e)return[];const t=e.getProviders(),o=[];for(const s of t)try{const t=e.getModels(s);for(const e of t)o.push({id:`${s}/${e.id}`,name:e.name||e.id})}catch{}return o}catch{return[]}}}}function f(e){switch(e){case"stop":return"end_turn";case"length":return"max_tokens";case"toolUse":return"tool_use";case"error":return"error";case"aborted":return"aborted";default:return e}}function y(e,t,o,s,n,r,a,i){const c=Date.now()-r;return e.totalCostUsd+=a.cost.total,e.totalTurns++,{type:"result",subtype:t,session_id:e.id,result:"success"===t?o:void 0,stop_reason:"success"===t?s:null,total_cost_usd:a.cost.total,duration_ms:c,num_turns:1,modelUsage:{[n]:{inputTokens:a.input,outputTokens:a.output,cacheReadInputTokens:a.cacheRead,cacheCreationInputTokens:a.cacheWrite,costUSD:a.cost.total}},...i?{errors:i}:{}}}function w(e,t){e.input+=t.input,e.output+=t.output,e.cacheRead+=t.cacheRead,e.cacheWrite+=t.cacheWrite,e.totalTokens+=t.totalTokens,e.cost.input+=t.cost.input,e.cost.output+=t.cost.output,e.cost.cacheRead+=t.cost.cacheRead,e.cost.cacheWrite+=t.cost.cacheWrite,e.cost.total+=t.cost.total}function x(e){return e}
@@ -168,6 +168,10 @@ export interface PiProviderConfig {
168
168
  costCacheRead?: number;
169
169
  /** Cost per 1M cache write tokens (USD) */
170
170
  costCacheWrite?: number;
171
+ /** Optional provider for Phase 3 summarization. Falls back to session provider if unset. */
172
+ summarizationProvider?: string;
173
+ /** Optional model ID for Phase 3 summarization. Falls back to session model if unset. */
174
+ summarizationModelId?: string;
171
175
  }
172
176
  export interface ModelUsageEntry {
173
177
  inputTokens: number;
@@ -134,6 +134,8 @@ memory:
134
134
  enabled: false # enable hybrid BM25 + semantic search over memory
135
135
  modelRef: "" # references a model in the models registry (for API key + base URL)
136
136
  embeddingModel: "text-embedding-3-small"
137
+ prefixQuery: "" # prefix template for query embeddings (use {content} placeholder)
138
+ prefixDocument: "" # prefix template for document embeddings (use {content} placeholder)
137
139
  embeddingDimensions: 1536 # 512, 768, or 1536 (native) — lower = faster, higher = more precise
138
140
  updateDebounceMs: 3000 # debounce for fs.watch before re-indexing
139
141
  embedIntervalMs: 300000 # interval (ms) between embedding cycles (5 min)
@@ -141,10 +143,16 @@ memory:
141
143
  maxSnippetChars: 700 # max chars per snippet in results
142
144
  maxInjectedChars: 4000 # max total chars injected into context from search results
143
145
  rrfK: 60 # RRF fusion constant (higher = smoother blending)
146
+ dir: "./memory" # memory storage directory (relative to gmabPath/data)
144
147
 
145
148
  agent:
146
- model: "Claude Opus" # model name or ID from the models registry
149
+ model: "Claude Opus" # model name or ID from the models registry (format: "Name:id")
147
150
  mainFallback: "" # fallback model if primary fails (name or ID)
151
+ # picoAgent — use external LLM providers (OpenRouter, OpenAI, etc.) via pi-ai instead of Claude SDK
152
+ picoAgent:
153
+ enabled: false
154
+ modelRefs: [] # list of "Name:provider:modelId" refs (e.g. "GPT:openrouter:openai/gpt-5.2")
155
+ rollingMemoryModel: "" # model for context summarization, full ref format (e.g. "MyModel:openrouter:openai/gpt-4.1-mini")
148
156
  maxTurns: 30
149
157
  permissionMode: "bypassPermissions"
150
158
  sessionTTL: 3600
@@ -265,6 +273,7 @@ agent:
265
273
  plugins: [] # local plugins, configurable via Nostromo UI
266
274
  inflightTyping: true # keep typing indicator active when multiple messages are in-flight
267
275
  autoApproveTools: true # auto-approve SDK tool permissions; when false, asks user via channel buttons
276
+ autoRenew: 0 # auto-renew session after N consecutive errors (0 = disabled)
268
277
 
269
278
  cron:
270
279
  enabled: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hera-al/server",
3
- "version": "1.6.5",
3
+ "version": "1.6.7",
4
4
  "private": false,
5
5
  "description": "Hera Artificial Life — Multi-channel AI agent gateway with autonomous capabilities",
6
6
  "license": "MIT",
@@ -85,6 +85,9 @@
85
85
  "yaml": "^2.7.0",
86
86
  "zod": "^4.0.0"
87
87
  },
88
+ "overrides": {
89
+ "fast-xml-parser": ">=5.3.6"
90
+ },
88
91
  "devDependencies": {
89
92
  "@types/better-sqlite3": "^7.6.12",
90
93
  "@types/node": "^22.13.4",