@hera-al/server 1.6.33 → 1.6.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/agent-service.d.ts +1 -0
- package/dist/agent/agent-service.js +1 -1
- package/dist/agent/prompt-builder.d.ts +10 -2
- package/dist/agent/prompt-builder.js +1 -1
- package/dist/agent/quality-gate.d.ts +17 -0
- package/dist/agent/quality-gate.js +1 -0
- package/dist/agent/session-agent.d.ts +4 -0
- package/dist/agent/session-agent.js +1 -1
- package/dist/agent/workspace-files.d.ts +57 -1
- package/dist/agent/workspace-files.js +1 -1
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +1 -1
- package/dist/nostromo/nostromo.js +1 -1
- package/dist/nostromo/ui-html-modals.js +1 -1
- package/dist/nostromo/ui-js-prompts.js +1 -1
- package/dist/server.js +1 -1
- package/installationPkg/AGENTS.md +61 -190
- package/installationPkg/BEHAVIOUR.md +92 -0
- package/installationPkg/SOUL.md +4 -3
- package/installationPkg/SYSTEM_PROMPT.md +2 -2
- package/installationPkg/SYSTEM_PROMPT_SUBAGENT.md +2 -2
- package/installationPkg/config.example.yaml +2 -0
- package/installationPkg/default-jobs.json +1 -1
- package/package.json +6 -6
|
@@ -52,6 +52,7 @@ export declare class AgentService {
|
|
|
52
52
|
* Check if a session has a pending interactive permission request.
|
|
53
53
|
*/
|
|
54
54
|
hasPendingPermission(sessionKey: string): boolean;
|
|
55
|
+
isFallbackActive(sessionKey: string): boolean;
|
|
55
56
|
/**
|
|
56
57
|
* Resolve a pending interactive permission request for a session.
|
|
57
58
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{query as s}from"@anthropic-ai/claude-agent-sdk";import{SessionAgent as o}from"./session-agent.js";import{createNodeToolsServer as t}from"../tools/node-tools.js";import{createMessageToolsServer as e}from"../tools/message-tools.js";import{createTelegramActionsToolsServer as r}from"../tools/telegram-actions-tools.js";import{createA2UIToolsServer as i}from"../tools/a2ui-tools.js";import{createDynamicUIToolsServer as n}from"../tools/dynamic-ui-tools.js";import{createStateToolsServer as a}from"../tools/state-tools.js";import{createLogger as l}from"../utils/logger.js";const c=l("AgentService");export class AgentService{config;agents=new Map;usageBySession=new Map;nodeToolsFactory=null;messageToolsFactory=null;serverToolsFactory=null;cronToolsFactory=null;ttsToolsFactory=null;memoryToolsFactory=null;browserToolsFactory=null;picoToolsFactory=null;telegramToolsFactory=null;a2uiToolsFactory=null;dynamicUIToolsFactory=null;plasmaClientToolsFactory=null;conceptToolsFactory=null;refToolServers=null;channelManager=null;nodeRegistry=null;showToolUseGetter=null;constructor(s,o,a,l,c,h,g,y,d,u,T,m,F){this.config=s,a&&(this.channelManager=a,this.telegramToolsFactory=()=>r(a,()=>this.config)),o&&(this.nodeRegistry=o,this.nodeToolsFactory=()=>t(o),this.a2uiToolsFactory=()=>i({nodeRegistry:o}),this.dynamicUIToolsFactory=()=>n({nodeRegistry:o})),a&&h&&(this.messageToolsFactory=()=>e(a,()=>this.config,h)),l&&(this.serverToolsFactory=l),c&&(this.cronToolsFactory=c),g&&(this.ttsToolsFactory=g),y&&(this.memoryToolsFactory=y),u&&(this.browserToolsFactory=u),T&&(this.picoToolsFactory=T),m&&(this.plasmaClientToolsFactory=m),F&&(this.conceptToolsFactory=F),d&&(this.showToolUseGetter=d)}async sendMessage(s,o,t,e,r,i,n,a,l,h){const g=this.getOrCreateAgent(s,t,e,r,i,n,a,l,h);if(i&&i!==g.getModel()){const t=s=>{const o=this.config.models.find(o=>o.id===s);return!(!o?.proxy||"not-used"===o.proxy)},y=t(g.getModel()),d=t(i);if(y||d){c.info(`[${s}] Proxy config change detected (old=${y}, new=${d}), restarting session`),this.destroySession(s);const t=this.getOrCreateAgent(s,void 0,e,r,i,n,a,l,h);return await t.send(o)}await g.setModel(i)}try{return await g.send(o)}catch(y){const d=y instanceof Error?y.message:String(y);if(d.includes("INVALID_ARGUMENT")||/API Error: 400/.test(d)){c.warn(`[${s}] Transient API 400 error, retrying once: ${d.slice(0,120)}`),this.agents.delete(s),g.close();const y=this.getOrCreateAgent(s,t,e,r,i,n,a,l,h);try{return await y.send(o)}catch(o){c.error(`[${s}] Retry also failed: ${o}`),this.agents.delete(s),y.close();const e=o instanceof Error?o.message:String(o);if(t)return{response:e.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw o}}if(this.agents.delete(s),g.close(),t)return c.warn(`Session agent failed for ${s}: ${y}`),{response:d.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw y}}hasNodeTools(){return null!==this.nodeToolsFactory}hasMessageTools(){return null!==this.messageToolsFactory}getToolServers(){if(!this.refToolServers){const s=[];this.nodeToolsFactory&&s.push(this.nodeToolsFactory()),this.messageToolsFactory&&s.push(this.messageToolsFactory()),this.serverToolsFactory&&s.push(this.serverToolsFactory()),this.cronToolsFactory&&s.push(this.cronToolsFactory()),this.ttsToolsFactory&&s.push(this.ttsToolsFactory()),this.memoryToolsFactory&&s.push(this.memoryToolsFactory()),this.browserToolsFactory&&s.push(this.browserToolsFactory()),this.picoToolsFactory&&s.push(this.picoToolsFactory()),this.telegramToolsFactory&&s.push(this.telegramToolsFactory()),this.a2uiToolsFactory&&s.push(this.a2uiToolsFactory()),this.dynamicUIToolsFactory&&s.push(this.dynamicUIToolsFactory()),this.plasmaClientToolsFactory&&s.push(this.plasmaClientToolsFactory()),this.conceptToolsFactory&&s.push(this.conceptToolsFactory()),s.push(a("__ref__",this.config.dataDir)),this.refToolServers=s}return this.refToolServers}getOrCreateAgent(s,t,e,r,i,l,c,h,g){const y=this.agents.get(s);if(y&&y.isActive())return y;let d,u;y&&(y.close(),this.agents.delete(s));const T=s.indexOf(":"),m=T>0?s.substring(0,T):void 0,F=T>0?s.substring(T+1):void 0;this.nodeRegistry&&(m&&F?d=n({nodeRegistry:this.nodeRegistry,channel:m,chatId:F}):this.dynamicUIToolsFactory&&(d=this.dynamicUIToolsFactory())),this.plasmaClientToolsFactory&&(u=this.plasmaClientToolsFactory(m,F));const p=a(s,this.config.dataDir),f=new o(s,this.config,e,r,t,i,this.nodeToolsFactory?.()??void 0,this.messageToolsFactory?.()??void 0,this.serverToolsFactory?.()??void 0,this.cronToolsFactory?.()??void 0,l,c,h,this.ttsToolsFactory?.()??void 0,this.memoryToolsFactory?.()??void 0,this.browserToolsFactory?.()??void 0,g,this.picoToolsFactory?.()??void 0,this.telegramToolsFactory?.()??void 0,this.a2uiToolsFactory?.()??void 0,d??void 0,u??void 0,this.conceptToolsFactory?.()??void 0,p);if(this.channelManager){const s=this.channelManager;if(f.setChannelSender(async(o,t,e,r)=>{r&&r.length>0?await s.sendButtons(o,t,e,[r]):await s.sendToChannel(o,t,e)}),this.showToolUseGetter){const o=this.showToolUseGetter;f.setToolUseNotifier(async(t,e,r)=>{if(!o(`${t}:${e}`))return;const i=`⚙️ Using ${r.replace(/^mcp__[^_]+__/,"")}`;await s.sendToChannel(t,e,i),await s.setTyping(t,e)})}f.setTypingSetter(async(o,t)=>{await s.setTyping(o,t)}),f.setTypingClearer(async(o,t)=>{await s.clearTyping(o,t)}),f.setTextBlockStreamer(async(o,t,e)=>{await s.sendResponse(o,t,e)})}return f.setUsageRecorder((s,o,t,e,r)=>{this.usageBySession.set(s,{totalCostUsd:o,durationMs:t,numTurns:e,modelUsage:r,recordedAt:Date.now()})}),this.agents.set(s,f),f}async interrupt(s){const o=this.agents.get(s);return!!o&&o.interrupt()}isBusy(s){const o=this.agents.get(s);return!!o&&o.isBusy()}hasPendingPermission(s){const o=this.agents.get(s);return!!o&&o.hasPendingPermission()}resolvePermission(s,o){const t=this.agents.get(s);t&&t.resolvePermission(o)}hasPendingQuestion(s){const o=this.agents.get(s);return!!o&&o.hasPendingQuestion()}resolveQuestion(s,o){const t=this.agents.get(s);t&&t.resolveQuestion(o)}destroySession(s){const o=this.agents.get(s);o&&(o.close(),this.agents.delete(s),c.info(`Session agent destroyed: ${s}`))}destroyAll(){for(const[s,o]of this.agents)o.close(),c.info(`Session agent destroyed (reconfigure): ${s}`);this.agents.clear()}getActiveSessions(){return Array.from(this.agents.keys()).filter(s=>{const o=this.agents.get(s);return o&&o.isActive()})}getActiveSessionCount(){return this.getActiveSessions().length}getUsage(s){return this.usageBySession.get(s)}getSdkSlashCommands(){for(const s of this.agents.values()){const o=s.getSdkSlashCommands();if(o.length>0)return o}return[]}async listModels(){try{const o=s({prompt:"list models",options:{maxTurns:0}}),t=await o.supportedModels();for await(const s of o)break;return t.map(s=>({id:s.id??s.name??String(s),name:s.name??s.id??String(s)}))}catch(s){return c.error(`Failed to list models: ${s}`),[{id:"claude-sonnet-4-6",name:"Claude Sonnet 4.6"},{id:"claude-opus-4-6",name:"Claude Opus 4.6"},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku 3.5"}]}}}
|
|
1
|
+
import{query as s}from"@anthropic-ai/claude-agent-sdk";import{SessionAgent as o}from"./session-agent.js";import{createNodeToolsServer as t}from"../tools/node-tools.js";import{createMessageToolsServer as e}from"../tools/message-tools.js";import{createTelegramActionsToolsServer as r}from"../tools/telegram-actions-tools.js";import{createA2UIToolsServer as i}from"../tools/a2ui-tools.js";import{createDynamicUIToolsServer as n}from"../tools/dynamic-ui-tools.js";import{createStateToolsServer as a}from"../tools/state-tools.js";import{createLogger as l}from"../utils/logger.js";const c=l("AgentService");export class AgentService{config;agents=new Map;usageBySession=new Map;nodeToolsFactory=null;messageToolsFactory=null;serverToolsFactory=null;cronToolsFactory=null;ttsToolsFactory=null;memoryToolsFactory=null;browserToolsFactory=null;picoToolsFactory=null;telegramToolsFactory=null;a2uiToolsFactory=null;dynamicUIToolsFactory=null;plasmaClientToolsFactory=null;conceptToolsFactory=null;refToolServers=null;channelManager=null;nodeRegistry=null;showToolUseGetter=null;constructor(s,o,a,l,c,h,g,y,d,u,T,m,F){this.config=s,a&&(this.channelManager=a,this.telegramToolsFactory=()=>r(a,()=>this.config)),o&&(this.nodeRegistry=o,this.nodeToolsFactory=()=>t(o),this.a2uiToolsFactory=()=>i({nodeRegistry:o}),this.dynamicUIToolsFactory=()=>n({nodeRegistry:o})),a&&h&&(this.messageToolsFactory=()=>e(a,()=>this.config,h)),l&&(this.serverToolsFactory=l),c&&(this.cronToolsFactory=c),g&&(this.ttsToolsFactory=g),y&&(this.memoryToolsFactory=y),u&&(this.browserToolsFactory=u),T&&(this.picoToolsFactory=T),m&&(this.plasmaClientToolsFactory=m),F&&(this.conceptToolsFactory=F),d&&(this.showToolUseGetter=d)}async sendMessage(s,o,t,e,r,i,n,a,l,h){const g=this.getOrCreateAgent(s,t,e,r,i,n,a,l,h);if(i&&i!==g.getModel()){const t=s=>{const o=this.config.models.find(o=>o.id===s);return!(!o?.proxy||"not-used"===o.proxy)},y=t(g.getModel()),d=t(i);if(y||d){c.info(`[${s}] Proxy config change detected (old=${y}, new=${d}), restarting session`),this.destroySession(s);const t=this.getOrCreateAgent(s,void 0,e,r,i,n,a,l,h);return await t.send(o)}await g.setModel(i)}try{return await g.send(o)}catch(y){const d=y instanceof Error?y.message:String(y);if(d.includes("INVALID_ARGUMENT")||/API Error: 400/.test(d)){c.warn(`[${s}] Transient API 400 error, retrying once: ${d.slice(0,120)}`),this.agents.delete(s),g.close();const y=this.getOrCreateAgent(s,t,e,r,i,n,a,l,h);try{return await y.send(o)}catch(o){c.error(`[${s}] Retry also failed: ${o}`),this.agents.delete(s),y.close();const e=o instanceof Error?o.message:String(o);if(t)return{response:e.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw o}}if(this.agents.delete(s),g.close(),t)return c.warn(`Session agent failed for ${s}: ${y}`),{response:d.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw y}}hasNodeTools(){return null!==this.nodeToolsFactory}hasMessageTools(){return null!==this.messageToolsFactory}getToolServers(){if(!this.refToolServers){const s=[];this.nodeToolsFactory&&s.push(this.nodeToolsFactory()),this.messageToolsFactory&&s.push(this.messageToolsFactory()),this.serverToolsFactory&&s.push(this.serverToolsFactory()),this.cronToolsFactory&&s.push(this.cronToolsFactory()),this.ttsToolsFactory&&s.push(this.ttsToolsFactory()),this.memoryToolsFactory&&s.push(this.memoryToolsFactory()),this.browserToolsFactory&&s.push(this.browserToolsFactory()),this.picoToolsFactory&&s.push(this.picoToolsFactory()),this.telegramToolsFactory&&s.push(this.telegramToolsFactory()),this.a2uiToolsFactory&&s.push(this.a2uiToolsFactory()),this.dynamicUIToolsFactory&&s.push(this.dynamicUIToolsFactory()),this.plasmaClientToolsFactory&&s.push(this.plasmaClientToolsFactory()),this.conceptToolsFactory&&s.push(this.conceptToolsFactory()),s.push(a("__ref__",this.config.dataDir)),this.refToolServers=s}return this.refToolServers}getOrCreateAgent(s,t,e,r,i,l,c,h,g){const y=this.agents.get(s);if(y&&y.isActive())return y;let d,u;y&&(y.close(),this.agents.delete(s));const T=s.indexOf(":"),m=T>0?s.substring(0,T):void 0,F=T>0?s.substring(T+1):void 0;this.nodeRegistry&&(m&&F?d=n({nodeRegistry:this.nodeRegistry,channel:m,chatId:F}):this.dynamicUIToolsFactory&&(d=this.dynamicUIToolsFactory())),this.plasmaClientToolsFactory&&(u=this.plasmaClientToolsFactory(m,F));const p=a(s,this.config.dataDir),f=new o(s,this.config,e,r,t,i,this.nodeToolsFactory?.()??void 0,this.messageToolsFactory?.()??void 0,this.serverToolsFactory?.()??void 0,this.cronToolsFactory?.()??void 0,l,c,h,this.ttsToolsFactory?.()??void 0,this.memoryToolsFactory?.()??void 0,this.browserToolsFactory?.()??void 0,g,this.picoToolsFactory?.()??void 0,this.telegramToolsFactory?.()??void 0,this.a2uiToolsFactory?.()??void 0,d??void 0,u??void 0,this.conceptToolsFactory?.()??void 0,p);if(this.channelManager){const s=this.channelManager;if(f.setChannelSender(async(o,t,e,r)=>{r&&r.length>0?await s.sendButtons(o,t,e,[r]):await s.sendToChannel(o,t,e)}),this.showToolUseGetter){const o=this.showToolUseGetter;f.setToolUseNotifier(async(t,e,r)=>{if(!o(`${t}:${e}`))return;const i=`⚙️ Using ${r.replace(/^mcp__[^_]+__/,"")}`;await s.sendToChannel(t,e,i),await s.setTyping(t,e)})}f.setTypingSetter(async(o,t)=>{await s.setTyping(o,t)}),f.setTypingClearer(async(o,t)=>{await s.clearTyping(o,t)}),f.setTextBlockStreamer(async(o,t,e)=>{await s.sendResponse(o,t,e)})}return f.setUsageRecorder((s,o,t,e,r)=>{this.usageBySession.set(s,{totalCostUsd:o,durationMs:t,numTurns:e,modelUsage:r,recordedAt:Date.now()})}),this.agents.set(s,f),f}async interrupt(s){const o=this.agents.get(s);return!!o&&o.interrupt()}isBusy(s){const o=this.agents.get(s);return!!o&&o.isBusy()}hasPendingPermission(s){const o=this.agents.get(s);return!!o&&o.hasPendingPermission()}isFallbackActive(s){const o=this.agents.get(s);return!!o&&o.isFallbackActive()}resolvePermission(s,o){const t=this.agents.get(s);t&&t.resolvePermission(o)}hasPendingQuestion(s){const o=this.agents.get(s);return!!o&&o.hasPendingQuestion()}resolveQuestion(s,o){const t=this.agents.get(s);t&&t.resolveQuestion(o)}destroySession(s){const o=this.agents.get(s);o&&(o.close(),this.agents.delete(s),c.info(`Session agent destroyed: ${s}`))}destroyAll(){for(const[s,o]of this.agents)o.close(),c.info(`Session agent destroyed (reconfigure): ${s}`);this.agents.clear()}getActiveSessions(){return Array.from(this.agents.keys()).filter(s=>{const o=this.agents.get(s);return o&&o.isActive()})}getActiveSessionCount(){return this.getActiveSessions().length}getUsage(s){return this.usageBySession.get(s)}getSdkSlashCommands(){for(const s of this.agents.values()){const o=s.getSdkSlashCommands();if(o.length>0)return o}return[]}async listModels(){try{const o=s({prompt:"list models",options:{maxTurns:0}}),t=await o.supportedModels();for await(const s of o)break;return t.map(s=>({id:s.id??s.name??String(s),name:s.name??s.id??String(s)}))}catch(s){return c.error(`Failed to list models: ${s}`),[{id:"claude-sonnet-4-6",name:"Claude Sonnet 4.6"},{id:"claude-opus-4-6",name:"Claude Opus 4.6"},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku 3.5"}]}}}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ProcessedMessage } from "../media/message-processor.js";
|
|
2
2
|
import type { AppConfig } from "../config.js";
|
|
3
3
|
import type { SessionContext } from "./session-agent.js";
|
|
4
|
-
import { type WorkspaceFile } from "./workspace-files.js";
|
|
4
|
+
import { type WorkspaceFile, type BuildMeta } from "./workspace-files.js";
|
|
5
5
|
export interface BuiltPrompt {
|
|
6
6
|
/** Text prompt for the agent (includes memory context, file refs, text) */
|
|
7
7
|
text: string;
|
|
@@ -43,6 +43,14 @@ export interface SystemPromptParams {
|
|
|
43
43
|
* all {{PLACEHOLDER}} variables.
|
|
44
44
|
*/
|
|
45
45
|
export declare function buildSystemPrompt(params: SystemPromptParams): string;
|
|
46
|
+
/**
|
|
47
|
+
* Build system prompt WITH build metadata for simulation/introspection.
|
|
48
|
+
* Returns both the prompt string and metadata about file includes, memory budget, truncations.
|
|
49
|
+
*/
|
|
50
|
+
export declare function buildSystemPromptWithMeta(params: SystemPromptParams): {
|
|
51
|
+
prompt: string;
|
|
52
|
+
meta: BuildMeta;
|
|
53
|
+
};
|
|
46
54
|
/**
|
|
47
55
|
* Resolve all {{PLACEHOLDER}} occurrences in a template string.
|
|
48
56
|
* Unknown placeholders are left as-is with a warning.
|
|
@@ -54,5 +62,5 @@ export declare function resolvePlaceholders(template: string, vars: Record<strin
|
|
|
54
62
|
*
|
|
55
63
|
* Note: session_info is no longer included here — it's in the system prompt.
|
|
56
64
|
*/
|
|
57
|
-
export declare function buildPrompt(processed: ProcessedMessage, memoryContext?: string, sessionMeta?: SessionMeta): BuiltPrompt;
|
|
65
|
+
export declare function buildPrompt(processed: ProcessedMessage, memoryContext?: string, sessionMeta?: SessionMeta, timezone?: string): BuiltPrompt;
|
|
58
66
|
//# sourceMappingURL=prompt-builder.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{hostname as e,type as n,release as t,arch as o}from"node:os";import{modelRefName as s}from"../config.js";import{loadTemplate as a,loadBuiltInTools as r,formatWorkspaceFiles as i,filterForSubagent as
|
|
1
|
+
import{hostname as e,type as n,release as t,arch as o}from"node:os";import{modelRefName as s}from"../config.js";import{loadTemplate as a,loadBuiltInTools as r,formatWorkspaceFiles as i,formatWorkspaceFilesWithMeta as l,filterForSubagent as c}from"./workspace-files.js";import{createLogger as m}from"../utils/logger.js";const d=m("PromptBuilder");export function buildSystemPrompt(l){const{config:m,sessionContext:f,mode:E,hasNodeTools:y,hasMessageTools:_,coderSkill:I,subagentTask:O,toolServers:A}=l,R="minimal"===E?"SYSTEM_PROMPT_SUBAGENT.md":"SYSTEM_PROMPT.md",N=a(m.dataDir,R),w="minimal"===E?c(l.workspaceFiles):l.workspaceFiles,b=resolvePlaceholders(N,{SESSION_KEY:f.sessionKey,CHANNEL:f.channel,CHAT_ID:f.chatId,SESSION_ID:f.sessionId||"(new session)",MODEL:s(m.agent.model),HOSTNAME:e(),OS:`${n()} ${t()} (${o()})`,WORKSPACE_DIR:m.agent.workspacePath,DATA_DIR:m.dataDir,MEMORY_FILE:f.memoryFile||"(memory disabled)",ATTACHMENTS_DIR:f.attachmentsDir||"(memory disabled)",TIMEZONE:m.timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,CURRENT_DATE:(new Date).toLocaleDateString("en-US",{timeZone:m.timezone||void 0,weekday:"long",year:"numeric",month:"long",day:"numeric"}),SUBAGENT_TASK:O??"",NODE_TOOLS_INSTRUCTIONS:y?h():"",MESSAGE_TOOLS_INSTRUCTIONS:_?u():"",HEARTBEAT_INSTRUCTIONS:m.cron.enabled?p(m.cron.heartbeat.message):"",HEARTBEAT_PROMPT:m.cron.enabled?m.cron.heartbeat.message:"",CLAUDE_BUILT_IN_TOOLS:I?"":r(m.dataDir,E),SEARCH_IN_MEMORIES:"builtin-only"!==m.memory.recallStrategy?g():"",AVAILABLE_TOOLS:S(A),RUNTIME_LINE:T(m,f),WORKSPACE_FILES:i(w,m.dataDir)}).replace(/\n{3,}/g,"\n\n");return d.debug(`System prompt built (mode=${E}, template=${R}, length=${b.length})`),b}export function buildSystemPromptWithMeta(i){const{config:m,sessionContext:d,mode:f,hasNodeTools:E,hasMessageTools:y,coderSkill:_,subagentTask:I,toolServers:O}=i,A="minimal"===f?"SYSTEM_PROMPT_SUBAGENT.md":"SYSTEM_PROMPT.md",R=a(m.dataDir,A),N="minimal"===f?c(i.workspaceFiles):i.workspaceFiles,{formatted:w,meta:b}=l(N,m.dataDir);return{prompt:resolvePlaceholders(R,{SESSION_KEY:d.sessionKey,CHANNEL:d.channel,CHAT_ID:d.chatId,SESSION_ID:d.sessionId||"(new session)",MODEL:s(m.agent.model),HOSTNAME:e(),OS:`${n()} ${t()} (${o()})`,WORKSPACE_DIR:m.agent.workspacePath,DATA_DIR:m.dataDir,MEMORY_FILE:d.memoryFile||"(memory disabled)",ATTACHMENTS_DIR:d.attachmentsDir||"(memory disabled)",TIMEZONE:m.timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,CURRENT_DATE:(new Date).toLocaleDateString("en-US",{timeZone:m.timezone||void 0,weekday:"long",year:"numeric",month:"long",day:"numeric"}),SUBAGENT_TASK:I??"",NODE_TOOLS_INSTRUCTIONS:E?h():"",MESSAGE_TOOLS_INSTRUCTIONS:y?u():"",HEARTBEAT_INSTRUCTIONS:m.cron.enabled?p(m.cron.heartbeat.message):"",HEARTBEAT_PROMPT:m.cron.enabled?m.cron.heartbeat.message:"",CLAUDE_BUILT_IN_TOOLS:_?"":r(m.dataDir,f),SEARCH_IN_MEMORIES:"builtin-only"!==m.memory.recallStrategy?g():"",AVAILABLE_TOOLS:S(O),RUNTIME_LINE:T(m,d),WORKSPACE_FILES:w}).replace(/\n{3,}/g,"\n\n"),meta:b}}export function resolvePlaceholders(e,n){return e.replace(/\{\{(\w+)\}\}/g,(e,t)=>t in n?n[t]:(d.warn(`Unknown placeholder: {{${t}}}`),`{{${t}}}`))}function h(){return"# Remote Nodes\n\nYou have access to remote nodes — external machines that you can control. Use the node tools to discover and interact with them.\n\n## Available tools\n\n- **list_nodes**: Call this to see which nodes are currently connected. Returns each node's ID, name, platform, hostname, and available commands. Always call this first before trying to execute commands, so you know which nodes are online and what their IDs are.\n\n- **node_exec**: Execute a command on a specific node. You must provide the nodeId (from list_nodes), the command name, and its parameters.\n\n## Supported commands\n\n- **shell.run**: Run a shell command on the node. Params: { cmd: string, args?: string[], cwd?: string, timeout?: number, env?: Record<string,string> }. Returns { stdout, stderr, exitCode }.\n- **shell.which**: Check if a binary exists on the node. Params: { cmd: string }. Returns { path } or null.\n\n## Guidelines\n\n- Always call list_nodes first to discover available nodes and their IDs. Do not guess node IDs.\n- When a user asks to run something on a remote machine, a node, or a specific hostname, call list_nodes to see what's online, then use node_exec with the appropriate nodeId.\n- If multiple nodes are connected, ask the user which one to use when the intent is ambiguous.\n- If no nodes are connected, inform the user that no remote nodes are available.\n- Report command results clearly: show stdout, note any stderr, and mention non-zero exit codes."}function u(){return"# Messaging\n\nYou have tools to send messages to chat channels. Each message you process includes a <session_info> block with the current channel and chatId.\n\n## Available tools\n\n- **send_message**: Send a text message to a specific channel and chat. Use the channel and chatId from the session context to reply on the current conversation. You can also send to a different channel or chatId if instructed.\n\n- **list_channels**: List all registered channels. Returns each channel's name and whether it is active.\n\n## Guidelines\n\n- Use send_message when you need to proactively send a message outside of the normal response flow (e.g. notifications, forwarding, or sending to a different chat).\n- Your normal response text is already delivered to the user. Only use send_message for additional messages or cross-channel communication.\n- The channel and chatId from the session context identify the current conversation. Use them to send follow-up messages to the same chat.\n- If the user asks you to message someone on a different channel or chat, use the appropriate channel name and chatId.\n- Never spam or send unsolicited messages. Only send when explicitly asked or when it is clearly part of the task."}function p(e){return`# Heartbeats\n\nHeartbeat prompt: ${e}\nIf you receive a heartbeat poll (a user message matching the heartbeat prompt above), and there is nothing that needs attention, reply exactly:\nHEARTBEAT_OK\nThe system treats a leading/trailing "HEARTBEAT_OK" as a heartbeat ack and may suppress it (not deliver to the user).\nIf something needs attention, do NOT include "HEARTBEAT_OK"; reply with the alert text instead.`}function g(){return"## Memory Search Tools\n\nYou have access to memory search tools for recalling past conversations and knowledge:\n\n- `memory_search` — semantically searches Markdown chunks (~400 token target, 80-token overlap) from `MEMORY.md` + `memory/**/*.md`. It returns snippet text (capped ~700 chars), file path, line range, and score. No full file payload is returned.\n- `memory_get` — reads a specific memory Markdown file (workspace-relative), optionally from a starting line and for N lines. Paths outside `MEMORY.md` / `memory/` are rejected.\n\nUse `memory_search` to find relevant past context before answering questions that might relate to previous conversations. Use `memory_get` to read the full content of a memory file when a search snippet is not enough."}function S(e){if(!e||0===e.length)return"";const n=[];for(const t of e){const e=t,o=e.instance?._registeredTools;if(o)for(const[e,t]of Object.entries(o)){const o=t.description||"",s=t.inputSchema?.def?.shape,a=[];if(s)for(const[e,n]of Object.entries(s)){let t=n.type||"unknown";"ZodNumber"===t?t="number":"ZodString"===t?t="string":"ZodBoolean"===t?t="boolean":"ZodArray"===t?t="array":"ZodObject"===t?t="object":"ZodEnum"===t&&(t="enum");const o=n.isOptional?.()??!1;a.push(`${e}${o?"?":""}: ${t}`)}const r=a.length>0?`Params: { ${a.join(", ")} }`:"No parameters.",i=o.match(/\.\s*(Returns?\s.+?)\.?\s*$/i),l=i?i[1].trim():"";let c=`- **${e}**: ${i?o.slice(0,i.index).trim():o.trim()}. ${r}`;l&&(c+=`. ${l}.`),n.push(c)}}return 0===n.length?"":n.join("\n")}function T(a,r){return`host=${e()} | os=${n()} ${t()} (${o()}) | model=${s(a.agent.model)} | channel=${r.channel} | session=${r.sessionKey}`}export function buildPrompt(e,n,t,o){const s=[],a=[];if(o){const n=e.contentBlocks.find(e=>"text"===e.type)?.text??"",t=/^\[.*\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(n),a=/Current time: /.test(n),r=n.startsWith("/");if(!t&&!a&&!r){const e=new Date,n=new Intl.DateTimeFormat("en-US",{timeZone:o,weekday:"short"}).format(e),t=e.toLocaleString("en-US",{timeZone:o,year:"numeric"}),a=e.toLocaleString("en-US",{timeZone:o,month:"2-digit"}),r=e.toLocaleString("en-US",{timeZone:o,day:"2-digit"}),i=e.toLocaleString("en-GB",{timeZone:o,hour:"2-digit",minute:"2-digit",hour12:!1});s.push(`[${n} ${t}-${a}-${r} ${i}]`)}}n&&s.push("<conversation_history>",n,"</conversation_history>","");for(const n of e.contentBlocks)"text"===n.type&&n.text?s.push(n.text):"image"===n.type&&n.imageBase64&&a.push({base64:n.imageBase64,mimeType:n.imageMimeType??"image/jpeg"});const r=e.savedFiles.filter(e=>!e.endsWith(".tgs"));if(r.length>0){s.push(""),s.push("Files available in the current working directory:");for(const e of r)s.push(`- ${e}`)}return{text:s.join("\n"),images:a}}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface QualityGateResult {
|
|
2
|
+
pass: boolean;
|
|
3
|
+
flags: string[];
|
|
4
|
+
/** Time taken in ms */
|
|
5
|
+
durationMs: number;
|
|
6
|
+
}
|
|
7
|
+
export interface QualityGateConfig {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
/** Skip gate for these channels (e.g. "cron") */
|
|
10
|
+
skipChannels: string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Run the quality gate on an agent response.
|
|
14
|
+
* Returns quickly (~1-2s with Haiku).
|
|
15
|
+
*/
|
|
16
|
+
export declare function runQualityGate(responseText: string): Promise<QualityGateResult>;
|
|
17
|
+
//# sourceMappingURL=quality-gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{query as e}from"@anthropic-ai/claude-agent-sdk";import{createLogger as t}from"../utils/logger.js";const s=t("QualityGate");export async function runQualityGate(t){const a=Date.now();try{const n={...process.env};delete n.ANTHROPIC_BETAS,delete n.CLAUDE_CODE_EXTRA_BODY;const o=e({prompt:`You are a factual claim verifier for an AI assistant that has access to tools (file reader, search, APIs, shell commands, email, calendar, etc.).\n\nYour job is to catch UNGROUNDED claims — things the assistant states as fact but clearly fabricated or guessed without checking. You must be PRECISE and avoid false positives.\n\n## FAIL — flag these patterns:\n\n1. **Fabricated existence/negation claims**: "That file doesn't exist", "there are no results", "nobody does X" — stated confidently as fact when the assistant is clearly guessing rather than reporting a check.\n2. **Invented temporal claims**: "3 days ago", "last Tuesday", "the next run is tomorrow" — when the assistant appears to be estimating dates/times from memory rather than reporting data.\n3. **Made-up statistics**: Specific numbers, percentages, or counts that appear invented (not quoted from a source, not from user context, not from a data output).\n4. **Confident claims about things the assistant clearly hasn't checked**: Saying "everything works" or "no errors" without any indication of having verified.\n\n## PASS — do NOT flag these:\n\n- **Data being reported from tool output**: If the response contains structured data (JSON, log lines, email content, file content, API responses), the assistant retrieved it via tools. Dates, numbers, names, and content within or clearly derived from such data are VERIFIED — do not flag them.\n- **Summarizing retrieved content**: If the assistant summarizes an email, a file, logs, or search results, the summary content is grounded in the retrieval. Do not flag it.\n- **Quoting the user's own words**: If a claim echoes what the user said in the conversation, it's grounded.\n- **Code references after reading code**: Line numbers, function names, variable values — if the assistant has been discussing code it read, these are grounded.\n- **Conversational/opinion responses**: No factual claims to verify.\n- **Questions**: The assistant is asking, not asserting.\n- **Hedged statements**: "I think", "probably", "I haven't checked", "let me verify".\n\n## Key principle:\nThe assistant uses tools extensively. If the response LOOKS like it's reporting tool output (contains specific data, quotes, structured info), assume it IS reporting tool output and PASS it. Only FAIL when the assistant is clearly making things up without any tool backing — the kind of confident bullshit that erodes trust.\n\nWhen in doubt, PASS. False positives (flagging correct responses) are MORE harmful than false negatives (missing a bad claim), because they waste compute on unnecessary re-prompts.\n\nRespond with ONLY a JSON object (no markdown, no explanation):\n{"pass": true} or {"pass": false, "flags": ["description of each unverified claim"]}\n\n---\n\nRESPONSE TO CHECK:\n${t}`,options:{model:"haiku",env:n,maxTurns:1}}),r=[];for await(const e of o)"result"===e.type&&"success"===e.subtype&&"result"in e&&r.push(e.result??"");const i=r.join("").trim(),c=Date.now()-a;try{const e=i.match(/\{[\s\S]*\}/);if(!e)return s.warn(`QualityGate: Could not parse response: ${i.slice(0,200)}`),{pass:!0,flags:[],durationMs:c};const t=JSON.parse(e[0]),a=!0===t.pass,n=Array.isArray(t.flags)?t.flags:[];return s.info(`QualityGate: ${a?"PASS":"FAIL"} (${c}ms)${a?"":` flags=${JSON.stringify(n)}`}`),{pass:a,flags:n,durationMs:c}}catch(e){return s.warn(`QualityGate: JSON parse error: ${e}`),{pass:!0,flags:[],durationMs:c}}}catch(e){const t=Date.now()-a;return s.error(`QualityGate error: ${e}`),{pass:!0,flags:[],durationMs:t}}}
|
|
@@ -69,6 +69,8 @@ export declare class SessionAgent {
|
|
|
69
69
|
private currentResponse;
|
|
70
70
|
private currentSessionId;
|
|
71
71
|
private model;
|
|
72
|
+
private primaryModel;
|
|
73
|
+
private fallbackActive;
|
|
72
74
|
private queueMode;
|
|
73
75
|
private closed;
|
|
74
76
|
/** Pi provider config — non-null when engine.type === "pi". */
|
|
@@ -100,6 +102,7 @@ export declare class SessionAgent {
|
|
|
100
102
|
private loopDetector;
|
|
101
103
|
private skillNudgeEnabled;
|
|
102
104
|
private skillNudgeThreshold;
|
|
105
|
+
private qualityGateEnabled;
|
|
103
106
|
private toolCallsCurrentTurn;
|
|
104
107
|
private toolCallsLastTurn;
|
|
105
108
|
private messageSentViaTool;
|
|
@@ -140,6 +143,7 @@ export declare class SessionAgent {
|
|
|
140
143
|
isActive(): boolean;
|
|
141
144
|
getSessionId(): string;
|
|
142
145
|
getModel(): string;
|
|
146
|
+
isFallbackActive(): boolean;
|
|
143
147
|
getSdkSlashCommands(): string[];
|
|
144
148
|
setChannelSender(sender: ChannelSender): void;
|
|
145
149
|
setToolUseNotifier(notifier: ToolUseNotifier): void;
|
|
@@ -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";import{ToolLoopDetector as i}from"./tool-loop-detector.js";const n=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;loopDetector;skillNudgeEnabled;skillNudgeThreshold;toolCallsCurrentTurn=0;toolCallsLastTurn=0;messageSentViaTool=!1;pendingPermission=null;pendingQuestion=null;constructor(e,o,r,l,a,h,u,d,c,p,g,m,f,y,$,b,v,T,w,S,k,R,K,x){this.sessionKey=e,this.config=o,this.currentSessionId=a??"";const _=h??o.agent.model;this.model=_?t(o,_):"",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.loopDetector=new i(e),this.skillNudgeEnabled=o.agent.skillNudge?.enabled??!0,this.skillNudgeThreshold=o.agent.skillNudge?.threshold??10,this.queue=new s,this.opts={...this.model?{model:this.model}:{},systemPrompt:g?{type:"preset",preset:"claude_code",append:r}:r,...o.agent.maxTurns>0?{maxTurns:o.agent.maxTurns}:{},cwd:o.agent.workspacePath,env:process.env,permissionMode:o.agent.permissionMode,allowDangerouslySkipPermissions:!1,...v?{sandbox:{enabled:!0,autoAllowBashIfSandboxed:!0,network:{allowLocalBinding:!0}}}:{},canUseTool:async(e,s)=>this.handleCanUseTool(e,s),hooks:{PreCompact:[{hooks:[async e=>{const s=e?.trigger??"auto";if(n.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=>{n.error(`[${e}] SDK stderr: ${s.trimEnd()}`)}};const P=o.agent.settingSources;"user"===P?this.opts.settingSources=["user"]:"project"===P?this.opts.settingSources=["project"]:"both"===P&&(this.opts.settingSources=["user","project"]);const C=o.agent.mainFallback;C&&(this.opts.fallbackModel=t(o,C)),o.agent.allowedTools.length>0&&(this.opts.allowedTools=o.agent.allowedTools),o.agent.disallowedTools.length>0&&(this.opts.disallowedTools=o.agent.disallowedTools);const A={};if(Object.keys(o.agent.mcpServers).length>0&&Object.assign(A,o.agent.mcpServers),u&&(A["node-tools"]=u),d&&(A["message-tools"]=d),c&&(A["server-tools"]=c),p&&(A["cron-tools"]=p),y&&(A["tts-tools"]=y),$&&(A["memory-tools"]=$),b&&(A["browser-tools"]=b),T&&(A["pico-tools"]=T),w&&(A["telegram-actions"]=w),S&&(A["a2ui-tools"]=S),k&&(A["dynamic-ui-tools"]=k),R&&(A["plasma-client-tools"]=R),K&&(A["concept-tools"]=K),x&&(A["state-tools"]=x),Object.keys(A).length>0&&(this.opts.mcpServers=A,this.opts.allowedTools&&this.opts.allowedTools.length>0))for(const e of Object.keys(A)){const s=`mcp__${e}__*`;this.opts.allowedTools.includes(s)||this.opts.allowedTools.push(s)}if(a&&(this.opts.resume=a),!1===m&&(this.opts.allowedTools&&this.opts.allowedTools.length>0?this.opts.allowedTools=this.opts.allowedTools.filter(e=>"Task"!==e):(this.opts.disallowedTools||(this.opts.disallowedTools=[]),this.opts.disallowedTools.includes("Task")||this.opts.disallowedTools.push("Task"))),f){const e={};for(const s of o.agent.customSubAgents){if(!s.enabled)continue;const t=s.expandContext?l+"\n\n"+s.prompt:s.prompt;e[s.name]={description:s.description,prompt:t,tools:s.tools,..."inherit"!==s.model?{model:s.model}:{}}}Object.keys(e).length>0&&(this.opts.agents=e)}const I=o.agent.plugins.filter(e=>e.enabled);I.length>0&&(this.opts.options={...this.opts.options,plugins:I.map(e=>({type:"local",path:e.path}))});const q=this.buildEnvForModel(this.model);this.opts.env=q.env,q.disableThinking&&(this.opts.maxThinkingTokens=0),this.piProviderConfig=this.resolvePiConfig(),this.piProviderConfig&&n.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 i=this.config.agent.picoAgent;if(i?.enabled&&Array.isArray(i.modelRefs)&&(o=i.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 n.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&&(n.info(`[${this.currentSessionId}] piModelRef auto-correction: baseURL is openrouter.ai, switching provider from "${a}" to "openrouter" (modelId: "${a}/${h}")`),h=`${a}/${h}`,a="openrouter"),n.info(`[${this.currentSessionId}] piModelRef resolved: provider="${a}", modelId="${h}", contextWindow=${u?.contextWindow??128e3}`);const p=i?.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&&n.info(`[${this.currentSessionId}] Summarization model resolved: ${d}/${c}`)}return{provider:a,modelId:h,apiKey:u?.apiKey||void 0,baseUrl:u?.baseURL||void 0,contextWindowTokens:u?.contextWindow||void 0,costInput:u?.costInput||void 0,costOutput:u?.costOutput||void 0,costCacheRead:u?.costCacheRead||void 0,costCacheWrite:u?.costCacheWrite||void 0,summarizationProvider:d,summarizationModelId:c}}static API_RETRY_RE=/API Error:\s*(?:500|502|503|529)|overloaded|internal server error/i;async send(e,s){const t=s??0;if(this.closed||this.outputDone)throw new Error("SessionAgent is closed");if(this.skillNudgeEnabled&&this.toolCallsLastTurn>=this.skillNudgeThreshold&&0===t){const s=`[System note: Your previous turn used ${this.toolCallsLastTurn} tool calls. If you discovered a reusable workflow or improved on an existing skill's approach, consider updating or creating a skill (Edit the SKILL.md file directly). Skip this if the task was inherently complex and doesn't generalize.]`;e={...e,text:s+"\n\n"+e.text},this.toolCallsLastTurn=0,n.info(`[${this.sessionKey}] Skill nudge injected (previous turn had ${this.skillNudgeThreshold}+ tool calls)`)}let o;switch(this.loopDetector.reset(),this.ensureInitialized(),this.queueMode){case"collect":o=await this.sendCollect(e);break;case"steer":o=await this.sendSteer(e);break;default:o=await this.sendDirect(e)}const i=o.fullResponse??o.response,r=SessionAgent.API_RETRY_RE.test(i),l=this.config.agent.apiRetry,a=l.maxAttempts;if(r&&t<a){const s=t+1,o=Math.min(l.baseDelayMs*2**t,l.maxDelayMs);if(n.warn(`[${this.sessionKey}] Transient API error detected, retry ${s}/${a} in ${o}ms`),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=`API temporarily unavailable, retrying (attempt ${s}/${a})...`;this.channelSender(t,o,e).catch(()=>{})}}}return await new Promise(e=>setTimeout(e,o)),this.send(e,s)}if(r&&t>=a&&(n.error(`[${this.sessionKey}] API error persists after ${a} retries`),this.channelSender)){const e=this.sessionKey.indexOf(":");if(e>0){const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if("cron"!==s){const e=`API did not respond after ${a} attempts. Please try again later.`;this.channelSender(s,t,e).catch(()=>{})}}}return o}async interrupt(){if(this.closed||!this.queryHandle)return!1;try{return await this.queryHandle.interrupt(),n.info(`[${this.sessionKey}] Interrupted`),!0}catch{return!1}}async setModel(e){if(this.queryHandle)try{await this.queryHandle.setModel(e),this.model=e,n.info(`[${this.sessionKey}] Model changed to ${e}`)}catch(e){n.error(`[${this.sessionKey}] Failed to set model: ${e}`)}}close(){if(this.closed)return;if(this.closed=!0,this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.debounceResolve&&(this.debounceResolve(),this.debounceResolve=null),this.queue.close(),this.queryHandle){const e=this.queryHandle;this.queryHandle=null,queueMicrotask(()=>{try{e.close()}catch{}})}const e=new Error("SessionAgent closed");for(const s of this.pendingResponses)s.reject(e);for(const s of this.collectBuffer)s.reject(e);for(const s of this.droppedResolvers)s.reject(e);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[],n.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="",n.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,n.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?(n.info(`[${this.sessionKey}] Permission approved: ${s.toolName}`),s.resolve({behavior:"allow",updatedInput:s.input})):(n.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,n.info(`[${this.sessionKey}] Question answered: "${e}" for "${s.questionText}"`),s.resolve(e)}async handleCanUseTool(e,s){if("AskUserQuestion"===e){if(!this.channelSender)return n.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 i=s?.questions;if(!Array.isArray(i)||0===i.length)return{behavior:"allow",updatedInput:s};const r={};for(const e of i){const i=e.question||"?",l=Array.isArray(e.options)?e.options:[],a=[];if(e.header&&a.push(`*${e.header}*`),a.push(i),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 n.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";n.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:i}});if(r[i]=d,this.typingSetter)try{await this.typingSetter(t,o)}catch{}}return n.info(`[${this.sessionKey}] AskUserQuestion answered: ${JSON.stringify(r)}`),{behavior:"allow",updatedInput:{questions:s.questions,answers:r}}}const t=this.loopDetector.check(e,s);if("circuit_break"===t.severity)return n.error(`[${this.sessionKey}] CIRCUIT BREAK: ${t.reason}`),{behavior:"deny",message:`[Loop detected] ${t.reason} Try a different approach or ask the user for help.`};if(this.autoApproveTools)return n.debug(`[${this.sessionKey}] Auto-approving tool: ${e}`),{behavior:"allow",updatedInput:s};if(!this.channelSender)return n.warn(`[${this.sessionKey}] No channel sender for interactive permission, auto-approving: ${e}`),{behavior:"allow",updatedInput:s};const o=this.sessionKey.indexOf(":");if(o<0)return{behavior:"allow",updatedInput:s};const i=this.sessionKey.substring(0,o),r=this.sessionKey.substring(o+1);if(!i||!r||"cron"===i)return{behavior:"allow",updatedInput:s};const l=[`[Permission Request] Tool: ${e}`];if("Bash"===e&&s?.command)l.push(`Command: ${s.command}`),s.description&&l.push(`Description: ${s.description}`);else if("Write"===e&&s?.file_path)l.push(`File: ${s.file_path}`);else if("Edit"===e&&s?.file_path)l.push(`File: ${s.file_path}`);else if("ExitPlanMode"===e&&s?.plan){if(l.push(""),l.push(s.plan),Array.isArray(s.allowedPrompts)&&s.allowedPrompts.length>0){l.push(""),l.push("Requested permissions:");for(const e of s.allowedPrompts)l.push(` - [${e.tool}] ${e.prompt}`)}}else{const e=JSON.stringify(s);e.length<=300?l.push(`Input: ${e}`):l.push(`Input: ${e.slice(0,297)}...`)}l.push(""),l.push("Reply: approve to allow, deny to reject");const a=l.join("\n"),h=[{text:"Approve",callbackData:"__tool_perm:approve"},{text:"Deny",callbackData:"__tool_perm:deny"}];try{await this.channelSender(i,r,a,h)}catch(e){return n.error(`[${this.sessionKey}] Failed to send permission request: ${e}`),{behavior:"allow",updatedInput:s}}if(this.typingClearer)try{await this.typingClearer(i,r)}catch{}const u=12e4;return new Promise(t=>{const o=setTimeout(()=>{this.pendingPermission?.resolve===t&&(this.pendingPermission=null,n.warn(`[${this.sessionKey}] Permission timeout for ${e}, auto-denying`),this.channelSender&&this.channelSender(i,r,`[Permission timeout] Tool ${e} denied after 120s`).catch(()=>{}),t({behavior:"deny",message:"Permission request timed out"}))},u);this.pendingPermission={resolve:t,toolName:e,input:s};const l=t;this.pendingPermission.resolve=e=>{clearTimeout(o),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 i=e?.questions;if(Array.isArray(i)){for(const e of i){const s=e.question||"?",i=Array.isArray(e.options)?e.options:[],r=[];if(e.header&&r.push(`*${e.header}*`),r.push(s),i.some(e=>e.description)){r.push("");for(const e of i){const s=e.description?`: ${e.description}`:"";r.push(`• ${e.label}${s}`)}}const l=r.join("\n");try{if(i.length>0){const e=i.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){n.error(`[${this.sessionKey}] Failed to forward AskUserQuestion: ${e}`)}}if(this.typingClearer)try{await this.typingClearer(t,o)}catch(e){n.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){n.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){n.error(`[${this.sessionKey}] Text block stream error: ${e}`)}}sendDirect(e){if(this.queueCap>0&&this.pendingResponses.length>=this.queueCap)return n.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),n.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(),n.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&&(n.info(`[${this.sessionKey}] Steer: interrupting current processing`),await this.interrupt()),this.sendDirect(e)}applyDropPolicy(e){if("new"===this.dropPolicy)return n.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}),n.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),i=[...this.droppedResolvers.splice(0),...e.map(e=>({resolve:e.resolve,reject:e.reject}))];this.droppedSummaries=[],this.pendingResponses.push({resolve:e=>{for(const s of i)s.resolve(e)},reject:e=>{for(const s of i)s.reject(e)}}),this.queue.push(o),n.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";n.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(),n.info(`[${this.sessionKey}] Pi engine initialized: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}catch(s){n.error(`[${this.sessionKey}] Failed to initialize Pi engine: ${s}`),n.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 n.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}),n.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(n.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(" "),n.info(`[${this.sessionKey}] Compact: ${this.currentResponse}`)}else if("init"!==t&&"status"!==t){const e=new Set(["task_started","task_notification","files_persisted","hook_started","hook_progress","hook_response"]),{type:o,...i}=s,r=JSON.stringify(i,null,2);e.has(t)?n.debug(`[${this.sessionKey}] Internal SDK event (${t}): ${r.slice(0,200)}`):(this.currentResponse=r,n.info(`[${this.sessionKey}] System message (${t??"unknown"}): ${r.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(", ");n.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){this.toolCallsCurrentTurn++,"mcp__message-tools__send_message"===e.name&&(this.messageSentViaTool=!0);const s=JSON.stringify(e.input);n.debug(`[${this.sessionKey}] Tool call: ${e.name} ${this.config.verboseDebugLogs?s:s.slice(0,100)+(s.length>100?"...":"")}`),this.toolUseNotifier&&"AskUserQuestion"!==e.name&&await this.notifyToolUse(e.name)}}if("tool_progress"===e.type){const s=e;n.debug(`[${this.sessionKey}] Tool progress: ${s.tool_name} (${s.elapsed_time_seconds}s)`)}if("result"===e.type){const s=e;let t;n.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&&!this.messageSentViaTool&&this.pendingResponses.length<=1&&(void 0!==s.total_cost_usd||s.usage)){const e=[];if(void 0!==s.total_cost_usd&&e.push(`Total cost: $${Number(s.total_cost_usd).toFixed(4)}`),void 0!==s.duration_ms&&e.push(`Duration: ${(s.duration_ms/1e3).toFixed(1)}s`),void 0!==s.num_turns&&e.push(`Turns: ${s.num_turns}`),s.modelUsage)for(const[t,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&&this.pendingResponses.length<=1){const e=this.piProviderConfig;n.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?(n.warn(`[${this.sessionKey}] Model refused the request`),this.currentResponse||(this.currentResponse="I'm unable to fulfill this request.")):"error"===o?(n.warn(`[${this.sessionKey}] Model returned stop_reason=error`),this.currentResponse||(this.currentResponse="⚠️ Request failed. The provider may be unavailable or the API key/credits may be exhausted.")):"max_tokens"===o&&n.warn(`[${this.sessionKey}] Response truncated: output token limit reached`)}else if("error_max_turns"===s.subtype)t="max_turns",n.warn(`[${this.sessionKey}] Max turns reached`);else if("error_max_budget_usd"===s.subtype)t="max_budget",n.warn(`[${this.sessionKey}] Max budget reached`);else{const e=s.errors??[];if(e.some(e=>e.includes("aborted")))n.info(`[${this.sessionKey}] Request aborted (steer interrupt)`);else if(n.error(`[${this.sessionKey}] SDK error: ${JSON.stringify(s)}`),!this.currentResponse&&!this.streamedAny){const s=e.filter(e=>!e.includes("aborted")).join("; ");this.currentResponse="⚠️ Request failed"+(s?`: ${s}`:". The provider may be unavailable or the API key/credits may be exhausted.")}}const i=this.pendingResponses.shift();if(i){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)),n.info(`[${this.sessionKey}] Response ready: session=${this.currentSessionId}, length=${s.length}${this.streamedAny?` (streamed, full=${e.length})`:""}`),i.resolve({response:s,fullResponse:this.streamedAny?e:void 0,sessionId:this.currentSessionId,sessionReset:!1,errorType:t,stopReason:o})}this.toolCallsLastTurn=this.toolCallsCurrentTurn,this.toolCallsCurrentTurn=0,this.messageSentViaTool=!1,this.currentResponse="",this.pendingTextBlock="",this.streamedAny=!1,this.streamedText="","collect"===this.queueMode&&(this.collectBuffer.length>0||this.droppedResolvers.length>0)&&await this.debounceThenFlush()}}}catch(e){n.error(`[${this.sessionKey}] Output stream error: ${e}`);const s=this.pendingResponses.shift();if(s)if(this.currentSessionId){const t=e instanceof Error?e.message:String(e);n.warn(`[${this.sessionKey}] Session error (${this.currentSessionId}): ${t}`),s.resolve({response:t,sessionId:"",sessionReset:!0})}else s.reject(e instanceof Error?e:new Error(String(e)));const t=new Error("SessionAgent terminated");for(const e of this.pendingResponses)e.reject(t);for(const e of this.collectBuffer)e.reject(t);for(const e of this.droppedResolvers)e.reject(t);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[]}finally{this.outputDone=!0}}}
|
|
1
|
+
import{query as e}from"@anthropic-ai/claude-agent-sdk";import{MessageQueue as s}from"./message-queue.js";import{resolveModelId as t,resolveModelEntry as i}from"../config.js";import{createLogger as o}from"../utils/logger.js";import{ToolLoopDetector as n}from"./tool-loop-detector.js";const r=o("SessionAgent");export class SessionAgent{sessionKey;config;queue;queryHandle=null;pendingResponses=[];currentResponse="";currentSessionId;model;primaryModel;fallbackActive=!1;queueMode;closed=!1;piProviderConfig=null;outputDone=!1;initialized=!1;opts;collectBuffer=[];lastCollectAt=0;debounceMs;debounceTimer=null;debounceResolve=null;queueCap;dropPolicy;droppedResolvers=[];droppedSummaries=[];sdkSlashCommands=[];channelSender=null;toolUseNotifier=null;typingSetter=null;typingClearer=null;textBlockStreamer=null;pendingTextBlock="";streamedAny=!1;streamedText="";usageRecorder=null;autoApproveTools;loopDetector;skillNudgeEnabled;skillNudgeThreshold;qualityGateEnabled;toolCallsCurrentTurn=0;toolCallsLastTurn=0;messageSentViaTool=!1;pendingPermission=null;pendingQuestion=null;constructor(e,i,o,l,a,h,u,d,c,p,g,f,m,y,$,b,v,T,w,S,k,R,K,x){this.sessionKey=e,this.config=i,this.currentSessionId=a??"";const _=h??i.agent.model;this.model=_?t(i,_):"",this.primaryModel=this.model,this.queueMode=i.agent.queueMode,this.debounceMs=Math.max(0,i.agent.queueDebounceMs),this.queueCap=Math.max(0,i.agent.queueCap),this.dropPolicy=i.agent.queueDropPolicy,this.autoApproveTools=i.agent.autoApproveTools,this.loopDetector=new n(e),this.skillNudgeEnabled=i.agent.skillNudge?.enabled??!0,this.skillNudgeThreshold=i.agent.skillNudge?.threshold??10,this.qualityGateEnabled=i.agent.qualityGate?.enabled??!1,this.queue=new s,this.opts={...this.model?{model:this.model}:{},systemPrompt:g?{type:"preset",preset:"claude_code",append:o}:o,...i.agent.maxTurns>0?{maxTurns:i.agent.maxTurns}:{},cwd:i.agent.workspacePath,env:process.env,permissionMode:i.agent.permissionMode,allowDangerouslySkipPermissions:!1,...v?{sandbox:{enabled:!0,autoAllowBashIfSandboxed:!0,network:{allowLocalBinding:!0}}}:{},canUseTool:async(e,s)=>this.handleCanUseTool(e,s),hooks:{PreCompact:[{hooks:[async e=>{const s=e?.trigger??"auto";if(r.info(`[${this.sessionKey}] PreCompact hook fired (trigger=${s})`),this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if("cron"!==t){const e="auto"===s?"The conversation context is getting large — compacting memory to keep things running smoothly.":"Compacting conversation memory...";this.channelSender(t,i,e).catch(()=>{})}}}return{}}]}]},stderr:s=>{r.error(`[${e}] SDK stderr: ${s.trimEnd()}`)}};const P=i.agent.settingSources;"user"===P?this.opts.settingSources=["user"]:"project"===P?this.opts.settingSources=["project"]:"both"===P&&(this.opts.settingSources=["user","project"]);const C=i.agent.mainFallback;C&&(this.opts.fallbackModel=t(i,C)),i.agent.allowedTools.length>0&&(this.opts.allowedTools=i.agent.allowedTools),i.agent.disallowedTools.length>0&&(this.opts.disallowedTools=i.agent.disallowedTools);const A={};if(Object.keys(i.agent.mcpServers).length>0&&Object.assign(A,i.agent.mcpServers),u&&(A["node-tools"]=u),d&&(A["message-tools"]=d),c&&(A["server-tools"]=c),p&&(A["cron-tools"]=p),y&&(A["tts-tools"]=y),$&&(A["memory-tools"]=$),b&&(A["browser-tools"]=b),T&&(A["pico-tools"]=T),w&&(A["telegram-actions"]=w),S&&(A["a2ui-tools"]=S),k&&(A["dynamic-ui-tools"]=k),R&&(A["plasma-client-tools"]=R),K&&(A["concept-tools"]=K),x&&(A["state-tools"]=x),Object.keys(A).length>0&&(this.opts.mcpServers=A,this.opts.allowedTools&&this.opts.allowedTools.length>0))for(const e of Object.keys(A)){const s=`mcp__${e}__*`;this.opts.allowedTools.includes(s)||this.opts.allowedTools.push(s)}if(a&&(this.opts.resume=a),!1===f&&(this.opts.allowedTools&&this.opts.allowedTools.length>0?this.opts.allowedTools=this.opts.allowedTools.filter(e=>"Task"!==e):(this.opts.disallowedTools||(this.opts.disallowedTools=[]),this.opts.disallowedTools.includes("Task")||this.opts.disallowedTools.push("Task"))),m){const e={};for(const s of i.agent.customSubAgents){if(!s.enabled)continue;const t=s.expandContext?l+"\n\n"+s.prompt:s.prompt;e[s.name]={description:s.description,prompt:t,tools:s.tools,..."inherit"!==s.model?{model:s.model}:{}}}Object.keys(e).length>0&&(this.opts.agents=e)}const I=i.agent.plugins.filter(e=>e.enabled);I.length>0&&(this.opts.options={...this.opts.options,plugins:I.map(e=>({type:"local",path:e.path}))});const q=this.buildEnvForModel(this.model);this.opts.env=q.env,q.disableThinking&&(this.opts.maxThinkingTokens=0),this.piProviderConfig=this.resolvePiConfig(),this.piProviderConfig&&r.info(`[${e}] Pi engine: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}resolvePiConfig(){const e=this.model,s=this.config.models?.find(s=>s.id===e),t=s?.name??"";let i;const o=this.config.agent.picoAgent;if(o?.enabled&&Array.isArray(o.modelRefs)&&(i=o.modelRefs.find(e=>e.split(":")[0]===t)),!i){const e=this.config.agent.engine;if(!e||"pi"!==e.type||!e.piModelRef)return null;i=e.piModelRef}const n=i.split(":");if(n.length<2)return r.warn(`[${this.currentSessionId}] Invalid piModelRef (missing ':'): ${i}`),null;const l=n[0].trim();let a,h;if(n.length>=3)a=n[1].trim(),h=n.slice(2).join(":").trim(),h.startsWith(a+":")&&(h=h.substring(a.length+1));else{const e=n[1].trim(),s=e.indexOf("/");s>0?(a=e.substring(0,s),h=e.substring(s+1)):(a="openrouter",h=e)}const u=this.config.models?.find(e=>e.name===l);let d,c;u?.baseURL&&u.baseURL.includes("openrouter.ai")&&"openrouter"!==a&&(r.info(`[${this.currentSessionId}] piModelRef auto-correction: baseURL is openrouter.ai, switching provider from "${a}" to "openrouter" (modelId: "${a}/${h}")`),h=`${a}/${h}`,a="openrouter"),r.info(`[${this.currentSessionId}] piModelRef resolved: provider="${a}", modelId="${h}", contextWindow=${u?.contextWindow??128e3}`);const p=o?.rollingMemoryModel;if(p){const e=p.split(":");if(e.length>=3)d=e[1].trim(),c=e.slice(2).join(":").trim();else if(2===e.length){const s=e[1].indexOf("/");s>0?(d=e[1].substring(0,s).trim(),c=e[1].substring(s+1).trim()):c=e[1].trim()}c&&r.info(`[${this.currentSessionId}] Summarization model resolved: ${d}/${c}`)}return{provider:a,modelId:h,apiKey:u?.apiKey||void 0,baseUrl:u?.baseURL||void 0,contextWindowTokens:u?.contextWindow||void 0,costInput:u?.costInput||void 0,costOutput:u?.costOutput||void 0,costCacheRead:u?.costCacheRead||void 0,costCacheWrite:u?.costCacheWrite||void 0,summarizationProvider:d,summarizationModelId:c}}static API_RETRY_RE=/API Error:\s*(?:500|502|503|529)|overloaded|internal server error/i;async send(e,s){const t=s??0;if(this.closed||this.outputDone)throw new Error("SessionAgent is closed");if(this.skillNudgeEnabled&&this.toolCallsLastTurn>=this.skillNudgeThreshold&&0===t){const s=`[System note: Your previous turn used ${this.toolCallsLastTurn} tool calls. If you discovered a reusable workflow or improved on an existing skill's approach, consider updating or creating a skill (Edit the SKILL.md file directly). Skip this if the task was inherently complex and doesn't generalize.]`;e={...e,text:s+"\n\n"+e.text},this.toolCallsLastTurn=0,r.info(`[${this.sessionKey}] Skill nudge injected (previous turn had ${this.skillNudgeThreshold}+ tool calls)`)}let o;switch(this.loopDetector.reset(),this.ensureInitialized(),this.queueMode){case"collect":o=await this.sendCollect(e);break;case"steer":o=await this.sendSteer(e);break;default:o=await this.sendDirect(e)}const n=o.fullResponse??o.response,l=SessionAgent.API_RETRY_RE.test(n),a=this.config.agent.apiRetry,h=a.maxAttempts;if(l&&t<h){const s=t+1,i=Math.min(a.baseDelayMs*2**t,a.maxDelayMs);if(r.warn(`[${this.sessionKey}] Transient API error detected, retry ${s}/${h} in ${i}ms`),this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if("cron"!==t){const e=`API temporarily unavailable, retrying (attempt ${s}/${h})...`;this.channelSender(t,i,e).catch(()=>{})}}}return await new Promise(e=>setTimeout(e,i)),this.send(e,s)}if(l&&t>=h){r.error(`[${this.sessionKey}] API error persists after ${h} retries`);const s=this.opts.fallbackModel;if(s&&!this.fallbackActive&&this.model!==s){const t=i(this.config,s);if(t?.types?.includes("internal")??!1){if(r.warn(`[${this.sessionKey}] Switching to fallback model: ${s}`),await this.setModel(s),this.fallbackActive=!0,this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const i=this.sessionKey.substring(0,e),o=this.sessionKey.substring(e+1);if("cron"!==i){const e=`⚠️ Primary model unavailable after ${h} retries. Switching to fallback (${t?.name??s}). Use /model or /new to restore.`;this.channelSender(i,o,e).catch(()=>{})}}}return this.send(e,0)}}if(this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if("cron"!==s){const e=`API did not respond after ${h} attempts. Please try again later.`;this.channelSender(s,t,e).catch(()=>{})}}}}const u=!0===e.__qualityGateRetry;if(!this.piProviderConfig&&this.qualityGateEnabled&&!u){const e=o.fullResponse??o.response;if(e&&e.length>50)try{const{runQualityGate:s}=await import("./quality-gate.js"),t=await s(e);if(!t.pass){r.warn(`[${this.sessionKey}] QualityGate FAILED (${t.durationMs}ms): ${JSON.stringify(t.flags)}`);const e={text:`[Quality Gate FAILED] Your previous response contains unverified claims:\n${t.flags.map(e=>`- ${e}`).join("\n")}\n\nReconsider your response. Verify the flagged claims (use tools if needed) and respond again with a corrected version. Do NOT mention the quality gate to the user.`,images:[],__qualityGateRetry:!0};r.info(`[${this.sessionKey}] QualityGate: re-prompting agent for self-correction`),o=await this.send(e,0)}}catch(e){r.warn(`[${this.sessionKey}] QualityGate error (fail-open): ${e}`)}}return o}async interrupt(){if(this.closed||!this.queryHandle)return!1;try{return await this.queryHandle.interrupt(),r.info(`[${this.sessionKey}] Interrupted`),!0}catch{return!1}}async setModel(e){if(this.queryHandle)try{await this.queryHandle.setModel(e),this.model=e,this.fallbackActive=!1,r.info(`[${this.sessionKey}] Model changed to ${e}`)}catch(e){r.error(`[${this.sessionKey}] Failed to set model: ${e}`)}}close(){if(this.closed)return;if(this.closed=!0,this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.debounceResolve&&(this.debounceResolve(),this.debounceResolve=null),this.queue.close(),this.queryHandle){const e=this.queryHandle;this.queryHandle=null,queueMicrotask(()=>{try{e.close()}catch{}})}const e=new Error("SessionAgent closed");for(const s of this.pendingResponses)s.reject(e);for(const s of this.collectBuffer)s.reject(e);for(const s of this.droppedResolvers)s.reject(e);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[],r.info(`[${this.sessionKey}] Closed`)}isActive(){return!this.closed&&!this.outputDone}getSessionId(){return this.currentSessionId}getModel(){return this.model}isFallbackActive(){return this.fallbackActive}getSdkSlashCommands(){return this.sdkSlashCommands}setChannelSender(e){this.channelSender=e}setToolUseNotifier(e){this.toolUseNotifier=e}setTypingSetter(e){this.typingSetter=e}setTypingClearer(e){this.typingClearer=e}setTextBlockStreamer(e){this.textBlockStreamer=e}setUsageRecorder(e){this.usageRecorder=e}buildEnvForModel(e){const s=this.config.models.find(s=>s.id===e);if(!s?.proxy||"not-used"===s.proxy)return{env:{...process.env},proxied:!1,disableThinking:!1};const t={...process.env};return"direct"===s.proxy?(t.ANTHROPIC_BASE_URL=s.baseURL,t.ANTHROPIC_AUTH_TOKEN=s.apiKey,t.ANTHROPIC_API_KEY="",r.info(`[${this.sessionKey}] Direct env applied for model ${e} (url=${t.ANTHROPIC_BASE_URL})`),{env:t,proxied:!0,disableThinking:!1}):(t.ANTHROPIC_BASE_URL=s.fastUrl||this.config.fastProxyUrl,t.ANTHROPIC_AUTH_TOKEN=s.fastProxyApiKey,t.ANTHROPIC_API_KEY="",delete t.ANTHROPIC_BETAS,delete t.CLAUDE_CODE_EXTRA_BODY,r.info(`[${this.sessionKey}] Proxy env applied for model ${e} (url=${t.ANTHROPIC_BASE_URL})`),{env:t,proxied:!0,disableThinking:!0})}hasPendingPermission(){return null!==this.pendingPermission}resolvePermission(e){if(!this.pendingPermission)return;const s=this.pendingPermission;this.pendingPermission=null,e?(r.info(`[${this.sessionKey}] Permission approved: ${s.toolName}`),s.resolve({behavior:"allow",updatedInput:s.input})):(r.info(`[${this.sessionKey}] Permission denied: ${s.toolName}`),s.resolve({behavior:"deny",message:"User denied this action"}))}isBusy(){return this.pendingResponses.length>0}hasPendingQuestion(){return null!==this.pendingQuestion}resolveQuestion(e){if(!this.pendingQuestion)return;const s=this.pendingQuestion;this.pendingQuestion=null,r.info(`[${this.sessionKey}] Question answered: "${e}" for "${s.questionText}"`),s.resolve(e)}async handleCanUseTool(e,s){if("AskUserQuestion"===e){if(!this.channelSender)return r.warn(`[${this.sessionKey}] No channel sender for AskUserQuestion, auto-approving`),{behavior:"allow",updatedInput:s};const e=this.sessionKey.indexOf(":");if(e<0)return{behavior:"allow",updatedInput:s};const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if(!t||!i||"cron"===t)return{behavior:"allow",updatedInput:s};const o=s?.questions;if(!Array.isArray(o)||0===o.length)return{behavior:"allow",updatedInput:s};const n={};for(const e of o){const o=e.question||"?",l=Array.isArray(e.options)?e.options:[],a=[];if(e.header&&a.push(`*${e.header}*`),a.push(o),l.some(e=>e.description)){a.push("");for(const e of l){const s=e.description?`: ${e.description}`:"";a.push(`• ${e.label}${s}`)}}const h=a.join("\n");if(this.typingClearer)try{await this.typingClearer(t,i)}catch{}try{if(l.length>0){const e=l.map(e=>({text:e.label||String(e),callbackData:`__ask:${e.label||String(e)}`}));await this.channelSender(t,i,h,e)}else await this.channelSender(t,i,h)}catch(e){return r.error(`[${this.sessionKey}] Failed to send AskUserQuestion: ${e}`),{behavior:"allow",updatedInput:s}}const u=55e3,d=await new Promise(e=>{const s=setTimeout(()=>{if(this.pendingQuestion){this.pendingQuestion=null;const s=l.length>0?l[0].label||String(l[0]):"No answer";r.warn(`[${this.sessionKey}] Question timeout, defaulting to "${s}"`),this.channelSender&&this.channelSender(t,i,`[Timeout] Auto-selected: ${s}`).catch(()=>{}),e(s)}},u);this.pendingQuestion={resolve:t=>{clearTimeout(s),e(t)},questionText:o}});if(n[o]=d,this.typingSetter)try{await this.typingSetter(t,i)}catch{}}return r.info(`[${this.sessionKey}] AskUserQuestion answered: ${JSON.stringify(n)}`),{behavior:"allow",updatedInput:{questions:s.questions,answers:n}}}const t=this.loopDetector.check(e,s);if("circuit_break"===t.severity)return r.error(`[${this.sessionKey}] CIRCUIT BREAK: ${t.reason}`),{behavior:"deny",message:`[Loop detected] ${t.reason} Try a different approach or ask the user for help.`};if(this.autoApproveTools)return r.debug(`[${this.sessionKey}] Auto-approving tool: ${e}`),{behavior:"allow",updatedInput:s};if(!this.channelSender)return r.warn(`[${this.sessionKey}] No channel sender for interactive permission, auto-approving: ${e}`),{behavior:"allow",updatedInput:s};const i=this.sessionKey.indexOf(":");if(i<0)return{behavior:"allow",updatedInput:s};const o=this.sessionKey.substring(0,i),n=this.sessionKey.substring(i+1);if(!o||!n||"cron"===o)return{behavior:"allow",updatedInput:s};const l=[`[Permission Request] Tool: ${e}`];if("Bash"===e&&s?.command)l.push(`Command: ${s.command}`),s.description&&l.push(`Description: ${s.description}`);else if("Write"===e&&s?.file_path)l.push(`File: ${s.file_path}`);else if("Edit"===e&&s?.file_path)l.push(`File: ${s.file_path}`);else if("ExitPlanMode"===e&&s?.plan){if(l.push(""),l.push(s.plan),Array.isArray(s.allowedPrompts)&&s.allowedPrompts.length>0){l.push(""),l.push("Requested permissions:");for(const e of s.allowedPrompts)l.push(` - [${e.tool}] ${e.prompt}`)}}else{const e=JSON.stringify(s);e.length<=300?l.push(`Input: ${e}`):l.push(`Input: ${e.slice(0,297)}...`)}l.push(""),l.push("Reply: approve to allow, deny to reject");const a=l.join("\n"),h=[{text:"Approve",callbackData:"__tool_perm:approve"},{text:"Deny",callbackData:"__tool_perm:deny"}];try{await this.channelSender(o,n,a,h)}catch(e){return r.error(`[${this.sessionKey}] Failed to send permission request: ${e}`),{behavior:"allow",updatedInput:s}}if(this.typingClearer)try{await this.typingClearer(o,n)}catch{}const u=12e4;return new Promise(t=>{const i=setTimeout(()=>{this.pendingPermission?.resolve===t&&(this.pendingPermission=null,r.warn(`[${this.sessionKey}] Permission timeout for ${e}, auto-denying`),this.channelSender&&this.channelSender(o,n,`[Permission timeout] Tool ${e} denied after 120s`).catch(()=>{}),t({behavior:"deny",message:"Permission request timed out"}))},u);this.pendingPermission={resolve:t,toolName:e,input:s};const l=t;this.pendingPermission.resolve=e=>{clearTimeout(i),l(e)}})}async forwardAskUserQuestion(e){if(!this.channelSender)return;const s=this.sessionKey.indexOf(":");if(s<0)return;const t=this.sessionKey.substring(0,s),i=this.sessionKey.substring(s+1);if(!t||!i||"cron"===t)return;const o=e?.questions;if(Array.isArray(o)){for(const e of o){const s=e.question||"?",o=Array.isArray(e.options)?e.options:[],n=[];if(e.header&&n.push(`*${e.header}*`),n.push(s),o.some(e=>e.description)){n.push("");for(const e of o){const s=e.description?`: ${e.description}`:"";n.push(`• ${e.label}${s}`)}}const l=n.join("\n");try{if(o.length>0){const e=o.map(e=>({text:e.label||String(e),callbackData:e.label||String(e)}));await this.channelSender(t,i,l,e)}else await this.channelSender(t,i,l)}catch(e){r.error(`[${this.sessionKey}] Failed to forward AskUserQuestion: ${e}`)}}if(this.typingClearer)try{await this.typingClearer(t,i)}catch(e){r.error(`[${this.sessionKey}] Failed to clear typing: ${e}`)}}}async notifyToolUse(e){if(!this.toolUseNotifier)return;const s=this.sessionKey.indexOf(":");if(s<0)return;const t=this.sessionKey.substring(0,s),i=this.sessionKey.substring(s+1);if(t&&i&&"cron"!==t)try{await this.toolUseNotifier(t,i,e)}catch(e){r.error(`[${this.sessionKey}] Failed to notify tool use: ${e}`)}}async flushPendingTextBlock(){if(!this.textBlockStreamer||!this.pendingTextBlock)return;const e=this.sessionKey.indexOf(":");if(e<0)return;const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if(!s||!t||"cron"===s)return;const i=this.pendingTextBlock;this.pendingTextBlock="",this.streamedAny=!0,this.streamedText+=i;try{await this.textBlockStreamer(s,t,i)}catch(e){r.error(`[${this.sessionKey}] Text block stream error: ${e}`)}}sendDirect(e){if(this.queueCap>0&&this.pendingResponses.length>=this.queueCap)return r.warn(`[${this.sessionKey}] Queue cap reached (${this.queueCap}), rejecting message`),Promise.resolve({response:"Queue is full. Please wait for the current processing to complete.",sessionId:this.currentSessionId,sessionReset:!1});const s=this.buildQueueMessage(e);return new Promise((e,t)=>{this.pendingResponses.push({resolve:e,reject:t}),this.queue.push(s),r.info(`[${this.sessionKey}] Message queued (pending=${this.pendingResponses.length})`)})}sendCollect(e){return this.pendingResponses.length>0?this.queueCap>0&&this.collectBuffer.length>=this.queueCap?this.applyDropPolicy(e):(this.lastCollectAt=Date.now(),r.info(`[${this.sessionKey}] Collecting message (buffer=${this.collectBuffer.length+1})`),new Promise((s,t)=>{this.collectBuffer.push({prompt:e,resolve:s,reject:t})})):this.sendDirect(e)}async sendSteer(e){return this.pendingResponses.length>0&&(r.info(`[${this.sessionKey}] Steer: interrupting current processing`),await this.interrupt()),this.sendDirect(e)}applyDropPolicy(e){if("new"===this.dropPolicy)return r.warn(`[${this.sessionKey}] Queue cap reached, rejecting new message`),Promise.resolve({response:"Queue is full. Please wait for the current processing to complete.",sessionId:this.currentSessionId,sessionReset:!1});const s=this.collectBuffer.shift();return"summarize"===this.dropPolicy&&this.droppedSummaries.push(function(e,s){const t=e.replace(/\s+/g," ").trim();return t.length<=s?t:`${t.slice(0,s-1).trimEnd()}…`}(s.prompt.text,140)),this.droppedResolvers.push({resolve:s.resolve,reject:s.reject}),r.warn(`[${this.sessionKey}] Queue cap reached, dropped oldest message (policy=${this.dropPolicy}, dropped=${this.droppedResolvers.length})`),this.lastCollectAt=Date.now(),new Promise((s,t)=>{this.collectBuffer.push({prompt:e,resolve:s,reject:t})})}async debounceThenFlush(){if(this.debounceMs<=0||this.closed)this.flushCollectBuffer();else{for(;!this.closed&&this.collectBuffer.length>0;){const e=Date.now()-this.lastCollectAt;if(e>=this.debounceMs)break;await new Promise(s=>{this.debounceResolve=s,this.debounceTimer=setTimeout(s,this.debounceMs-e)}),this.debounceTimer=null,this.debounceResolve=null}this.closed||this.flushCollectBuffer()}}flushCollectBuffer(){if(0===this.collectBuffer.length&&0===this.droppedResolvers.length)return;const e=this.collectBuffer.splice(0),s=e.map(e=>e.prompt),t=this.mergePrompts(s),i=this.buildQueueMessage(t),o=[...this.droppedResolvers.splice(0),...e.map(e=>({resolve:e.resolve,reject:e.reject}))];this.droppedSummaries=[],this.pendingResponses.push({resolve:e=>{for(const s of o)s.resolve(e)},reject:e=>{for(const s of o)s.reject(e)}}),this.queue.push(i),r.info(`[${this.sessionKey}] Flushed ${e.length} collected message(s) as one prompt`)}mergePrompts(e){const s=[],t=[];if(this.droppedSummaries.length>0){s.push(`[${this.droppedSummaries.length} earlier message(s) dropped due to queue cap]`);for(const e of this.droppedSummaries)s.push(`- ${e}`);s.push("")}if(1===e.length&&0===this.droppedSummaries.length)return e[0];if(e.length>0){s.push("[Queued messages while agent was busy]");for(let i=0;i<e.length;i++)e[i].text&&s.push(`${i+1}. ${e[i].text}`),t.push(...e[i].images)}return{text:s.join("\n"),images:t}}ensureInitialized(){if(this.initialized)return;this.initialized=!0;const s=this.piProviderConfig?"pi":"claudecode";r.info(`[${this.sessionKey}] Starting agent: engine=${s}, model=${this.model}, mode=${this.queueMode}, debounce=${this.debounceMs}ms, cap=${this.queueCap||"unlimited"}, drop=${this.dropPolicy}, session=${this.currentSessionId||"new"}`),this.piProviderConfig?this.initPiEngine():(this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput())}async initPiEngine(){try{const e=await import("../pi-agent-provider/index.js"),s=await e.createToolRegistryFromOptions(this.opts);this.queryHandle=e.piQuery({prompt:this.queue,options:this.opts},this.piProviderConfig,s),this.processOutput(),r.info(`[${this.sessionKey}] Pi engine initialized: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}catch(s){r.error(`[${this.sessionKey}] Failed to initialize Pi engine: ${s}`),r.warn(`[${this.sessionKey}] Falling back to Claude SDK (claudecode engine)`),this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput()}}buildQueueMessage(e){if(0===e.images.length)return r.debug(`[${this.sessionKey}] SDK request: text-only (${e.text.length} chars): ${this.config.verboseDebugLogs?e.text:e.text.slice(0,15)+"..."}`),{type:"user",message:{role:"user",content:e.text}};const s=[];for(const t of e.images)s.push({type:"image",source:{type:"base64",media_type:t.mimeType,data:t.base64}});return e.text&&s.push({type:"text",text:e.text}),r.debug(`[${this.sessionKey}] SDK request: ${s.length} block(s) [${s.map(e=>"image"===e.type?`image/${e.source.media_type}`:`text(${e.text?.length??0})`).join(", ")}]`),{type:"user",message:{role:"user",content:s}}}async processOutput(){if(this.queryHandle)try{for await(const e of this.queryHandle){if(this.closed)break;if(r.debug(`[${this.sessionKey}] SDK message: type=${e.type}, subtype=${e.subtype??"-"}, keys=${Object.keys(e).join(",")}`),"system"===e.type){const s=e,t=s.subtype;if("init"===t){const e=s.slash_commands;Array.isArray(e)&&(this.sdkSlashCommands=e.map(e=>e.replace(/^\//,"")))}if("compact_boundary"===t){const e=s.compact_metadata,t=["Context compacted."];e?.pre_tokens&&t.push(`Pre-compaction tokens: ${e.pre_tokens}.`),e?.trigger&&t.push(`Trigger: ${e.trigger}.`),this.currentResponse=t.join(" "),r.info(`[${this.sessionKey}] Compact: ${this.currentResponse}`)}else if("init"!==t&&"status"!==t){const e=new Set(["task_started","task_notification","files_persisted","hook_started","hook_progress","hook_response"]),{type:i,...o}=s,n=JSON.stringify(o,null,2);e.has(t)?r.debug(`[${this.sessionKey}] Internal SDK event (${t}): ${n.slice(0,200)}`):(this.currentResponse=n,r.info(`[${this.sessionKey}] System message (${t??"unknown"}): ${n.slice(0,200)}`))}}if("assistant"===e.type){const s=e.message.content,t=s.filter(e=>"text"===e.type).map(e=>e.text).join("");t&&(this.currentResponse=t,this.pendingTextBlock=t);const i=s.map(e=>e.type).join(", ");r.debug(`[${this.sessionKey}] SDK assistant message: blocks=[${i}], text length=${t.length}: ${this.config.verboseDebugLogs?t:t.slice(0,15)+"..."}`);s.some(e=>"tool_use"===e.type)&&this.pendingTextBlock&&this.textBlockStreamer&&!this.qualityGateEnabled&&await this.flushPendingTextBlock();for(const e of s)if("tool_use"===e.type){this.toolCallsCurrentTurn++,"mcp__message-tools__send_message"===e.name&&(this.messageSentViaTool=!0);const s=JSON.stringify(e.input);r.debug(`[${this.sessionKey}] Tool call: ${e.name} ${this.config.verboseDebugLogs?s:s.slice(0,100)+(s.length>100?"...":"")}`),this.toolUseNotifier&&"AskUserQuestion"!==e.name&&await this.notifyToolUse(e.name)}}if("tool_progress"===e.type){const s=e;r.debug(`[${this.sessionKey}] Tool progress: ${s.tool_name} (${s.elapsed_time_seconds}s)`)}if("result"===e.type){const s=e;let t;r.debug(`[${this.sessionKey}] SDK result: subtype=${s.subtype}, stop_reason=${s.stop_reason??"null"}, session=${s.session_id??"n/a"}, result length=${s.result?.length??0}`),"session_id"in s&&(this.currentSessionId=s.session_id),this.usageRecorder&&(void 0!==s.total_cost_usd||s.modelUsage)&&this.usageRecorder(this.sessionKey,s.total_cost_usd,s.duration_ms,s.num_turns,s.modelUsage);const i=s.stop_reason??null;if("success"===s.subtype){if(s.result)this.currentResponse=s.result;else if(!this.currentResponse&&!this.messageSentViaTool&&this.pendingResponses.length<=1&&(void 0!==s.total_cost_usd||s.usage)){const e=[];if(void 0!==s.total_cost_usd&&e.push(`Total cost: $${Number(s.total_cost_usd).toFixed(4)}`),void 0!==s.duration_ms&&e.push(`Duration: ${(s.duration_ms/1e3).toFixed(1)}s`),void 0!==s.num_turns&&e.push(`Turns: ${s.num_turns}`),s.modelUsage)for(const[t,i]of Object.entries(s.modelUsage)){const s=i,o=[` ${t}:`];s.inputTokens&&o.push(`input=${s.inputTokens}`),s.outputTokens&&o.push(`output=${s.outputTokens}`),s.cacheReadInputTokens&&o.push(`cache_read=${s.cacheReadInputTokens}`),s.cacheCreationInputTokens&&o.push(`cache_create=${s.cacheCreationInputTokens}`),void 0!==s.costUSD&&o.push(`cost=$${Number(s.costUSD).toFixed(4)}`),e.push(o.join(" "))}e.length>0&&(this.currentResponse=e.join("\n"))}if(!s.result&&!this.currentResponse&&this.pendingResponses.length<=1){const e=this.piProviderConfig;r.warn(`[${this.sessionKey}] Empty response on success: provider=${e?.provider??"sdk"}, modelId=${e?.modelId??"n/a"}, stop_reason=${i}. Check provider routing and API key.`)}"refusal"===i?(r.warn(`[${this.sessionKey}] Model refused the request`),this.currentResponse||(this.currentResponse="I'm unable to fulfill this request.")):"error"===i?(r.warn(`[${this.sessionKey}] Model returned stop_reason=error`),this.currentResponse||(this.currentResponse="⚠️ Request failed. The provider may be unavailable or the API key/credits may be exhausted.")):"max_tokens"===i&&r.warn(`[${this.sessionKey}] Response truncated: output token limit reached`)}else if("error_max_turns"===s.subtype)t="max_turns",r.warn(`[${this.sessionKey}] Max turns reached`);else if("error_max_budget_usd"===s.subtype)t="max_budget",r.warn(`[${this.sessionKey}] Max budget reached`);else{const e=s.errors??[];if(e.some(e=>e.includes("aborted")))r.info(`[${this.sessionKey}] Request aborted (steer interrupt)`);else if(r.error(`[${this.sessionKey}] SDK error: ${JSON.stringify(s)}`),!this.currentResponse&&!this.streamedAny){const s=e.filter(e=>!e.includes("aborted")).join("; ");this.currentResponse="⚠️ Request failed"+(s?`: ${s}`:". The provider may be unavailable or the API key/credits may be exhausted.")}}const o=this.pendingResponses.shift();if(o){const e=this.currentResponse||"";let s=e;this.streamedAny&&(s=this.pendingTextBlock||"",!s&&e&&e.length>this.streamedText.length&&(s=e.startsWith(this.streamedText)?e.slice(this.streamedText.length).replace(/^\n+/,""):e)),r.info(`[${this.sessionKey}] Response ready: session=${this.currentSessionId}, length=${s.length}${this.streamedAny?` (streamed, full=${e.length})`:""}`),o.resolve({response:s,fullResponse:this.streamedAny?e:void 0,sessionId:this.currentSessionId,sessionReset:!1,errorType:t,stopReason:i})}this.toolCallsLastTurn=this.toolCallsCurrentTurn,this.toolCallsCurrentTurn=0,this.messageSentViaTool=!1,this.currentResponse="",this.pendingTextBlock="",this.streamedAny=!1,this.streamedText="","collect"===this.queueMode&&(this.collectBuffer.length>0||this.droppedResolvers.length>0)&&await this.debounceThenFlush()}}}catch(e){r.error(`[${this.sessionKey}] Output stream error: ${e}`);const s=this.pendingResponses.shift();if(s)if(this.currentSessionId){const t=e instanceof Error?e.message:String(e);r.warn(`[${this.sessionKey}] Session error (${this.currentSessionId}): ${t}`),s.resolve({response:t,sessionId:"",sessionReset:!0})}else s.reject(e instanceof Error?e:new Error(String(e)));const t=new Error("SessionAgent terminated");for(const e of this.pendingResponses)e.reject(t);for(const e of this.collectBuffer)e.reject(t);for(const e of this.droppedResolvers)e.reject(t);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[]}finally{this.outputDone=!0}}}
|
|
@@ -4,6 +4,46 @@ export interface WorkspaceFile {
|
|
|
4
4
|
content: string;
|
|
5
5
|
missing: boolean;
|
|
6
6
|
}
|
|
7
|
+
export interface FileIncludeMeta {
|
|
8
|
+
/** The placeholder as written, e.g. "{{FILE:BEHAVIOUR.md}}" */
|
|
9
|
+
placeholder: string;
|
|
10
|
+
/** The included file name */
|
|
11
|
+
fileName: string;
|
|
12
|
+
/** Whether the file was found and resolved */
|
|
13
|
+
resolved: boolean;
|
|
14
|
+
/** Character count of the included content (0 if not found) */
|
|
15
|
+
chars: number;
|
|
16
|
+
/** Which workspace file contained this include */
|
|
17
|
+
sourceFile: string;
|
|
18
|
+
}
|
|
19
|
+
export interface MemoryBudgetMeta {
|
|
20
|
+
fileName: string;
|
|
21
|
+
totalChars: number;
|
|
22
|
+
hotChars: number;
|
|
23
|
+
coldChars: number;
|
|
24
|
+
hasHotMarkers: boolean;
|
|
25
|
+
/** Budget threshold that triggered the split */
|
|
26
|
+
budgetThreshold: number;
|
|
27
|
+
/** Whether the budget was exceeded (split happened) */
|
|
28
|
+
budgetExceeded: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface TruncationMeta {
|
|
31
|
+
fileName: string;
|
|
32
|
+
originalChars: number;
|
|
33
|
+
truncatedChars: number;
|
|
34
|
+
skippedChars: number;
|
|
35
|
+
}
|
|
36
|
+
export interface BuildMeta {
|
|
37
|
+
fileIncludes: FileIncludeMeta[];
|
|
38
|
+
memoryBudget: MemoryBudgetMeta | null;
|
|
39
|
+
truncations: TruncationMeta[];
|
|
40
|
+
/** All workspace files loaded (name + chars) */
|
|
41
|
+
loadedFiles: Array<{
|
|
42
|
+
name: string;
|
|
43
|
+
chars: number;
|
|
44
|
+
excluded: boolean;
|
|
45
|
+
}>;
|
|
46
|
+
}
|
|
7
47
|
/**
|
|
8
48
|
* Seed workspace files and system prompt templates on first run.
|
|
9
49
|
*
|
|
@@ -36,7 +76,7 @@ export declare function truncateContent(content: string, fileName: string, maxCh
|
|
|
36
76
|
* if it exceeds MEMORY_HOT_BUDGET, only sections marked with <!-- hot --> are loaded.
|
|
37
77
|
* Cold sections remain accessible via memory_search.
|
|
38
78
|
*/
|
|
39
|
-
export declare function formatWorkspaceFiles(files: WorkspaceFile[]): string;
|
|
79
|
+
export declare function formatWorkspaceFiles(files: WorkspaceFile[], dataDir?: string): string;
|
|
40
80
|
/**
|
|
41
81
|
* Extract only <!-- hot --> ... <!-- /hot --> sections from a file.
|
|
42
82
|
* If no hot markers exist, returns the full content (backward compatible).
|
|
@@ -44,6 +84,22 @@ export declare function formatWorkspaceFiles(files: WorkspaceFile[]): string;
|
|
|
44
84
|
* (Aurora lesson #4: memory budget awareness)
|
|
45
85
|
*/
|
|
46
86
|
export declare function extractHotSections(content: string, fileName: string): string;
|
|
87
|
+
/**
|
|
88
|
+
* Scan workspace file content for {{FILE:XXX.md}} references without resolving them.
|
|
89
|
+
* Used by Nostromo UI to discover included files.
|
|
90
|
+
*/
|
|
91
|
+
export declare function scanFileIncludes(files: WorkspaceFile[]): Array<{
|
|
92
|
+
fileName: string;
|
|
93
|
+
sourceFile: string;
|
|
94
|
+
}>;
|
|
95
|
+
/**
|
|
96
|
+
* Format workspace files with full build metadata for simulation/introspection.
|
|
97
|
+
* Returns both the formatted prompt string and metadata about the build process.
|
|
98
|
+
*/
|
|
99
|
+
export declare function formatWorkspaceFilesWithMeta(files: WorkspaceFile[], dataDir?: string): {
|
|
100
|
+
formatted: string;
|
|
101
|
+
meta: BuildMeta;
|
|
102
|
+
};
|
|
47
103
|
/**
|
|
48
104
|
* Load built-in tools from CBINT.json and format for the system prompt.
|
|
49
105
|
* Returns a formatted list of tools for the given mode (full = main agent, minimal = sub agent).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFileSync as
|
|
1
|
+
import{readFileSync as e,copyFileSync as t,existsSync as n,mkdirSync as o}from"node:fs";import{join as r}from"node:path";import{createLogger as s}from"../utils/logger.js";import{resolveInstallPkgDir as a}from"../utils/package-paths.js";const c=s("WorkspaceFiles"),l=["AGENTS.md","SOUL.md","TOOLS.md","IDENTITY.md","USER.md","HEARTBEAT.md","BOOTSTRAP.md"],i=["SYSTEM_PROMPT.md","SYSTEM_PROMPT_SUBAGENT.md","CBINT.json"],u=["MEMORY.md","memory.md"],m=new Set(["AGENTS.md","TOOLS.md","SOUL.md","IDENTITY.md","USER.md"]),d=8e3;export function ensureWorkspaceFiles(e){o(e,{recursive:!0});const s=g(),a=r(e,".templates");o(a,{recursive:!0});for(const o of l){const a=r(e,o);if(!n(a)){const e=r(s,o);n(e)&&(t(e,a),c.info(`Seeded ${o} → ${a}`))}}for(const e of i){const o=r(a,e);if(!n(o)){const a=r(s,e);n(a)&&(t(a,o),c.info(`Seeded template ${e} → ${o}`))}}}export function loadWorkspaceFiles(t){const o=[];for(const s of l){const a=r(t,s);if(n(a))try{const t=e(a,"utf-8");o.push({name:s,path:a,content:t,missing:!1})}catch(e){c.warn(`Failed to read ${a}: ${e}`),o.push({name:s,path:a,content:"",missing:!0})}}for(const s of u){const a=r(t,s);if(n(a)){try{const t=e(a,"utf-8");o.push({name:s,path:a,content:t,missing:!1})}catch(e){c.warn(`Failed to read ${a}: ${e}`)}break}}return o}export function filterForSubagent(e){return e.filter(e=>m.has(e.name))}export function truncateContent(e,t,n=2e4){if(e.length<=n)return e;const o=Math.floor(.7*n),r=Math.floor(.2*n),s=e.slice(0,o),a=e.slice(-r),l=e.length-o-r;return c.debug(`Truncated ${t}: ${e.length} → ${o+r} chars (skipped ${l})`),`${s}\n\n[... ${l} characters omitted from ${t} ...]\n\n${a}`}const f=new Set(["HEARTBEAT.md","BOOTSTRAP.md"]),h=/\{\{FILE:([A-Za-z0-9_\-]+\.md)\}\}/g;export function formatWorkspaceFiles(t,o){const s=t.filter(e=>!e.missing&&e.content.trim().length>0&&!f.has(e.name));return 0===s.length?"(No workspace files found)":s.map(t=>{let s=t.content;return o&&s.includes("{{FILE:")&&(s=function(t,o){return t.replace(h,(t,s)=>{const a=r(o,s);if(!n(a))return c.warn(`File include not found: {{FILE:${s}}} → ${a}`),`\x3c!-- FILE:${s} not found --\x3e`;try{const t=e(a,"utf-8");return c.info(`Resolved {{FILE:${s}}} (${t.length} chars)`),t}catch(e){return c.warn(`Failed to read included file ${s}: ${e}`),`\x3c!-- FILE:${s} read error --\x3e`}})}(s,o)),p(t.name)&&s.length>d&&(s=extractHotSections(s,t.name)),truncateContent(s,t.name)}).join("\n\n")}function p(e){return u.includes(e)}export function extractHotSections(e,t){const n="\x3c!-- hot --\x3e",o="\x3c!-- /hot --\x3e",r=[];let s=0;for(;s<e.length;){const t=e.indexOf(n,s);if(-1===t)break;const a=t+12,c=e.indexOf(o,a);if(-1===c){r.push(e.slice(a).trim());break}r.push(e.slice(a,c).trim()),s=c+13}if(0===r.length)return e;const a=r.join("\n\n"),l=e.length-a.length;return c.info(`Memory budget: ${t} ${e.length} chars → ${a.length} hot, ${l} cold (via memory_search)`),`${a}\n\n[... ${l} characters of cold memory omitted from ${t} — available via memory_search ...]`}export function scanFileIncludes(e){const t=[];for(const n of e){if(n.missing||!n.content)continue;let e;const o=/\{\{FILE:([A-Za-z0-9_\-]+\.md)\}\}/g;for(;null!==(e=o.exec(n.content));)t.push({fileName:e[1],sourceFile:n.name})}return t}export function formatWorkspaceFilesWithMeta(t,o){const s={fileIncludes:[],memoryBudget:null,truncations:[],loadedFiles:[]},a=t.filter(e=>!e.missing&&e.content.trim().length>0&&!f.has(e.name));for(const e of t)s.loadedFiles.push({name:e.name,chars:e.content?.length??0,excluded:e.missing||f.has(e.name)||!e.content?.trim()});if(0===a.length)return{formatted:"(No workspace files found)",meta:s};return{formatted:a.map(t=>{let a=t.content;if(o&&a.includes("{{FILE:")&&(a=a.replace(h,(a,c)=>{const l=r(o,c);if(!n(l))return s.fileIncludes.push({placeholder:`{{FILE:${c}}}`,fileName:c,resolved:!1,chars:0,sourceFile:t.name}),`\x3c!-- FILE:${c} not found --\x3e`;try{const n=e(l,"utf-8");return s.fileIncludes.push({placeholder:`{{FILE:${c}}}`,fileName:c,resolved:!0,chars:n.length,sourceFile:t.name}),n}catch{return s.fileIncludes.push({placeholder:`{{FILE:${c}}}`,fileName:c,resolved:!1,chars:0,sourceFile:t.name}),`\x3c!-- FILE:${c} read error --\x3e`}})),p(t.name)){const e=a.includes("\x3c!-- hot --\x3e"),n=a.length>d;if(n&&e){const o=a.length;a=extractHotSections(a,t.name);const r=a.lastIndexOf("[..."),c=r>=0?r:a.length;s.memoryBudget={fileName:t.name,totalChars:o,hotChars:c,coldChars:o-c,hasHotMarkers:e,budgetThreshold:d,budgetExceeded:n}}else s.memoryBudget={fileName:t.name,totalChars:a.length,hotChars:a.length,coldChars:0,hasHotMarkers:e,budgetThreshold:d,budgetExceeded:n}}if(a.length>2e4){const e=a.length;a=truncateContent(a,t.name),s.truncations.push({fileName:t.name,originalChars:e,truncatedChars:a.length,skippedChars:e-a.length})}return a}).join("\n\n"),meta:s}}export function loadBuiltInTools(e,t){const n=loadTemplateOrEmpty(e,"CBINT.json");if(!n.trim())return"";try{const e=JSON.parse(n),o="full"===t?{...e.tools_main_agent||{},...e.tools_sub_agent||{}}:e.tools_sub_agent||{},r=Object.entries(o);if(0===r.length)return"";const s=[];for(const[e,t]of r)s.push(`- **${e}**: ${t}`);return s.join("\n")}catch(e){return c.warn(`Failed to parse CBINT.json: ${e}`),""}}export function loadTemplateOrEmpty(t,o){const s=r(t,".templates",o);if(n(s))try{return e(s,"utf-8")}catch{return""}const a=r(g(),o);if(n(a))try{return e(a,"utf-8")}catch{return""}return""}export function loadTemplate(t,o){const s=r(t,".templates",o);if(n(s))return e(s,"utf-8");const a=r(g(),o);if(n(a))return c.warn(`Template ${o} not found in ${t}/.templates/, using bundled seed`),e(a,"utf-8");throw new Error(`Template ${o} not found in ${t}/.templates/ or bundled seeds`)}function g(){return a()}
|
package/dist/commands/status.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export class StatusCommand{provider;name="status";description="Show current server status (models, coder, nodes)";constructor(e){this.provider=e}async execute(e){const o=this.provider(e.sessionKey),s=[];s.push("Server Status"),s.push(` Config default: ${o.configDefaultModel}`);const n=o.agentModelName?`${o.agentModelName} (${o.agentModel})`:o.agentModel;if(s.push(` Agent model: ${n}`),o.fallbackModel){const e=o.fallbackModelName?`${o.fallbackModelName} (${o.fallbackModel})`:o.fallbackModel;s.push(` Fallback model: ${e}`)}else s.push(" Fallback model: (none)");if(s.push(" Coder skill: "+(o.coderSkill?"on":"off")),s.push(" Show tool use: "+(o.showToolUse?"on":"off")),s.push(" SubAgents: "+(o.subagentsEnabled?"on":"off")),s.push(" Custom SubAgents: "+(o.customSubAgentsEnabled?"on":"off")),s.push(" Sandbox: "+(o.sandboxEnabled?"on":"off")),o.connectedNodes.length>0){s.push(` Connected nodes (${o.connectedNodes.length}):`);for(const e of o.connectedNodes){const o=e.displayName??e.nodeId,n=e.hostname?` (${e.hostname})`:"";s.push(` - ${o}${n}`)}}else s.push(" Connected nodes: none");return{text:s.join("\n")}}}
|
|
1
|
+
export class StatusCommand{provider;name="status";description="Show current server status (models, coder, nodes)";constructor(e){this.provider=e}async execute(e){const o=this.provider(e.sessionKey),s=[];s.push("Server Status"),s.push(` Config default: ${o.configDefaultModel}`);const n=o.agentModelName?`${o.agentModelName} (${o.agentModel})`:o.agentModel;if(s.push(` Agent model: ${n}`),o.fallbackModel){const e=o.fallbackModelName?`${o.fallbackModelName} (${o.fallbackModel})`:o.fallbackModel;s.push(` Fallback model: ${e}`)}else s.push(" Fallback model: (none)");if(o.fallbackActive&&s.push(" ⚠️ FALLBACK ACTIVE — primary model was unavailable. Use /model or /new to restore."),s.push(" Coder skill: "+(o.coderSkill?"on":"off")),s.push(" Show tool use: "+(o.showToolUse?"on":"off")),s.push(" SubAgents: "+(o.subagentsEnabled?"on":"off")),s.push(" Custom SubAgents: "+(o.customSubAgentsEnabled?"on":"off")),s.push(" Sandbox: "+(o.sandboxEnabled?"on":"off")),o.connectedNodes.length>0){s.push(` Connected nodes (${o.connectedNodes.length}):`);for(const e of o.connectedNodes){const o=e.displayName??e.nodeId,n=e.hostname?` (${e.hostname})`:"";s.push(` - ${o}${n}`)}}else s.push(" Connected nodes: none");return{text:s.join("\n")}}}
|
package/dist/config.d.ts
CHANGED
|
@@ -435,6 +435,9 @@ declare const AppConfigSchema: z.ZodObject<{
|
|
|
435
435
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
436
436
|
threshold: z.ZodDefault<z.ZodNumber>;
|
|
437
437
|
}, z.core.$strip>>;
|
|
438
|
+
qualityGate: z.ZodDefault<z.ZodObject<{
|
|
439
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
440
|
+
}, z.core.$strip>>;
|
|
438
441
|
}, z.core.$strip>>>;
|
|
439
442
|
cron: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
440
443
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
package/dist/config.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFileSync as e,writeFileSync as t,existsSync as o,mkdirSync as a,renameSync as n,unlinkSync as l}from"node:fs";import{resolve as r,join as s}from"node:path";import{homedir as i}from"node:os";import{config as u}from"dotenv";import{parse as d,stringify as c}from"yaml";import{z as f}from"zod";import{BrowserConfigSchema as p}from"@hera-al/browser-server/config";import{createLogger as m}from"./utils/logger.js";const b=m("Config");function g(e){return"~"===e||e.startsWith("~/")?e.replace("~",i()):e}let h=g(process.env.GMAB_PATH??"~/gmab"),y=s(h,"data");export function getGmabPath(){return h}export function getDataDir(){return y}export function getNostromoKeyPath(){return s(y,".nostromo-key")}export function backupConfig(a){if(o(a))try{const r=e=>`${a}.backup${e}`;o(r(1))&&l(r(1));for(let e=2;e<=5;e++)o(r(e))&&n(r(e),r(e-1));t(r(5),e(a)),b.debug(`Config backup created: ${r(5)}`)}catch(e){b.warn(`Failed to create config backup: ${e}`)}}const v=f.record(f.string(),f.any()),x=f.object({reactions:f.boolean().default(!0),sendMessage:f.boolean().default(!0),editMessage:f.boolean().default(!0),deleteMessage:f.boolean().default(!0),sticker:f.boolean().default(!1),createForumTopic:f.boolean().default(!1)}),w=f.object({maxAttempts:f.number().default(3),baseDelayMs:f.number().default(1e3),maxDelayMs:f.number().default(3e4)}),M=f.enum(["off","dm","group","all","allowlist"]),k=f.object({botToken:f.string(),dmPolicy:f.enum(["open","token","allowlist"]).default("allowlist"),allowFrom:f.array(f.union([f.string(),f.number()])).default([]),name:f.string().optional(),reactionLevel:f.enum(["off","ack","minimal","extensive"]).default("ack"),reactionNotifications:f.enum(["off","own","all"]).default("off"),inlineButtonsScope:M.optional(),textChunkLimit:f.number().default(4e3),streamMode:f.enum(["off","partial","block"]).default("partial"),linkPreview:f.boolean().default(!0),actions:x.optional(),retry:w.optional(),timeoutSeconds:f.number().optional(),proxy:f.string().optional()}),j=f.object({enabled:f.boolean().default(!1),accounts:f.record(f.string(),k).default({})}),P=f.object({enabled:f.boolean().default(!1),accounts:v.default({})}),R=f.object({enabled:f.boolean().default(!0),port:f.number().default(3004)}),C=f.object({telegram:j.optional().default({enabled:!1,accounts:{}}),whatsapp:P.optional().default({enabled:!1,accounts:{}}),discord:P.optional().default({enabled:!1,accounts:{}}),slack:P.optional().default({enabled:!1,accounts:{}}),signal:P.optional().default({enabled:!1,accounts:{}}),msteams:P.optional().default({enabled:!1,accounts:{}}),googlechat:P.optional().default({enabled:!1,accounts:{}}),line:P.optional().default({enabled:!1,accounts:{}}),matrix:P.optional().default({enabled:!1,accounts:{}}),responses:R.optional().default({enabled:!0,port:3e3})}),S=f.object({modelRef:f.string().default(""),model:f.string().default("whisper-1"),language:f.string().default("")}),T=f.object({binaryPath:f.string().default("whisper"),model:f.string().default("base")}),A=f.object({enabled:f.boolean().default(!1),provider:f.string().default("openai-whisper"),"openai-whisper":S.optional().default({modelRef:"",model:"whisper-1",language:""}),"local-whisper":T.optional().default({binaryPath:"whisper",model:"base"})}),D=f.object({voice:f.string().default("en-US-MichelleNeural"),lang:f.string().default("en-US"),outputFormat:f.string().default("audio-24khz-48kbitrate-mono-mp3")}),I=f.object({modelRef:f.string().default(""),model:f.string().default("gpt-4o-mini-tts"),voice:f.string().default("alloy")}),U=f.object({modelRef:f.string().default(""),voiceId:f.string().default("pMsXgVXv3BLzUgSXRplE"),modelId:f.string().default("eleven_multilingual_v2")}),z=f.object({enabled:f.boolean().default(!1),provider:f.enum(["edge","openai","elevenlabs"]).default("openai"),maxTextLength:f.number().default(4096),timeoutMs:f.number().default(3e4),edge:D.optional().default({voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"}),openai:I.optional().default({modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"}),elevenlabs:U.optional().default({modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"})}),L=f.object({enabled:f.boolean().default(!1),embeddingModel:f.string().default("text-embedding-3-small"),embeddingDimensions:f.number().default(1536),modelRef:f.string().default(""),prefixQuery:f.string().default(""),prefixDocument:f.string().default(""),updateDebounceMs:f.number().default(3e3),embedIntervalMs:f.number().default(3e5),maxResults:f.number().default(6),maxSnippetChars:f.number().default(700),maxInjectedChars:f.number().default(4e3),rrfK:f.number().default(60)}),W={enabled:!1,embeddingModel:"text-embedding-3-small",embeddingDimensions:1536,modelRef:"",prefixQuery:"",prefixDocument:"",updateDebounceMs:3e3,embedIntervalMs:3e5,maxResults:6,maxSnippetChars:700,maxInjectedChars:4e3,rrfK:60},E=f.object({enabled:f.boolean().default(!0),model:f.string().default("")}),F={enabled:!0,model:""},K=f.object({enabled:f.boolean().default(!0),recallStrategy:f.enum(["builtin-only","search"]).default("builtin-only"),search:L.optional().default(W),l0:E.optional().default(F)}),O=f.object({command:f.string(),args:f.array(f.string()).optional(),env:f.record(f.string(),f.string()).optional()}).passthrough(),_=f.object({id:f.string(),name:f.string(),types:f.array(f.enum(["internal","external","env-var"])).optional().default(["external"]),proxy:f.enum(["not-used","direct","proxied"]).optional().default("not-used"),fastUrl:f.string().optional().default(""),fastProxyApiKey:f.string().optional().default(""),apiKey:f.string().optional().default(""),baseURL:f.string().optional().default(""),useEnvVar:f.string().optional().default(""),contextWindow:f.number().optional().default(2e5),costInput:f.number().optional().default(0),costOutput:f.number().optional().default(0),costCacheRead:f.number().optional().default(0),costCacheWrite:f.number().optional().default(0)}),q=[{id:"claude-opus-4-6",name:"Claude Opus",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-sonnet-4-6",name:"Claude Sonnet",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0}],B=f.array(_).default(q),G=f.object({name:f.string(),path:f.string(),description:f.string().default(""),enabled:f.boolean().default(!1)}),N=f.object({name:f.string(),description:f.string(),prompt:f.string(),model:f.enum(["sonnet","opus","haiku","inherit"]).default("inherit"),tools:f.array(f.string()).default(["Read","Write","Edit","Glob","Grep","WebSearch","WebFetch"]),expandContext:f.boolean().default(!1),enabled:f.boolean().default(!1)}),X=f.object({type:f.enum(["claudecode","pi"]).default("claudecode"),piModelRef:f.string().default("")}).passthrough(),$={type:"claudecode",piModelRef:""},V=f.object({enabled:f.boolean().default(!1),modelRefs:f.array(f.string()).default([]),rollingMemoryModel:f.string().default("")}),H={enabled:!1,modelRefs:[],rollingMemoryModel:""},Q=f.object({maxAttempts:f.number().default(5),baseDelayMs:f.number().default(2e3),maxDelayMs:f.number().default(3e4)}),Z={maxAttempts:5,baseDelayMs:2e3,maxDelayMs:3e4},J=f.object({model:f.string().default("claude-opus-4-6"),mainFallback:f.string().default(""),engine:X.optional().default($),picoAgent:V.optional().default(H),maxTurns:f.number().default(50),permissionMode:f.string().default("bypassPermissions"),sessionTTL:f.number().default(3600),queueMode:f.enum(["queue","collect","steer"]).default("steer"),queueDebounceMs:f.number().default(1500),queueCap:f.number().default(20),queueDropPolicy:f.enum(["old","new","summarize"]).default("summarize"),allowedTools:f.array(f.string()).default([]),disallowedTools:f.array(f.string()).default([]),mcpServers:f.record(f.string(),O).default({}),workspacePath:f.string().default("./workspace"),builtinCoderSkill:f.boolean().default(!1),settingSources:f.enum(["nothing","user","project","both"]).default("project"),customSubAgents:f.array(N).default([]),plugins:f.array(G).default([]),inflightTyping:f.boolean().default(!0),autoApproveTools:f.boolean().default(!0),autoRenew:f.number().default(0),apiRetry:Q.optional().default(Z),skillNudge:f.object({enabled:f.boolean().default(!0),threshold:f.number().default(10)}).default({enabled:!0,threshold:10})}),Y={enabled:!1,provider:"openai",maxTextLength:4096,timeoutMs:3e4,edge:{voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"},openai:{modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"},elevenlabs:{modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"}},ee={enabled:!0,recallStrategy:"builtin-only",dir:"",search:W,l0:F},te={model:"claude-opus-4-6",mainFallback:"",engine:$,picoAgent:H,maxTurns:50,permissionMode:"bypassPermissions",sessionTTL:3600,queueMode:"steer",queueDebounceMs:1500,queueCap:20,queueDropPolicy:"summarize",allowedTools:[],disallowedTools:[],mcpServers:{},workspacePath:"./workspace",builtinCoderSkill:!1,settingSources:"project",customSubAgents:[],plugins:[],inflightTyping:!0,autoApproveTools:!0,autoRenew:0,apiRetry:Z,skillNudge:{enabled:!0,threshold:10}},oe=f.object({enabled:f.boolean().default(!1),every:f.number().default(18e5),channel:f.string().default(""),chatId:f.string().default(""),message:f.string().default(""),ackMaxChars:f.number().default(300)}),ae={enabled:!1,every:18e5,channel:"",chatId:"",message:"",ackMaxChars:300},ne=f.object({enabled:f.boolean().default(!0),isolated:f.boolean().default(!0),broadcastEvents:f.boolean().default(!1),storePath:f.string().default(""),heartbeat:oe.optional().default(ae)}),le={enabled:!0,isolated:!0,broadcastEvents:!1,storePath:"",heartbeat:ae},re=f.object({enabled:f.boolean().default(!0),port:f.number().default(3001),basePath:f.string().default("/nostromo"),configCheckInterval:f.number().default(5),autoRestart:f.boolean().default(!0)}),se={enabled:!0,port:3001,basePath:"/nostromo",configCheckInterval:5,autoRestart:!0},ie=f.object({gmabPath:f.string().optional().default("~/gmab"),host:f.string().optional().default("127.0.0.1"),logLevel:f.enum(["debug","info","warn","error"]).optional().default("info"),verboseDebugLogs:f.boolean().optional().default(!0),timezone:f.string().optional().default(""),fastProxyUrl:f.string().optional().default("http://localhost:4181"),channels:C.optional().default({telegram:{enabled:!1,accounts:{}},whatsapp:{enabled:!1,accounts:{}},discord:{enabled:!1,accounts:{}},slack:{enabled:!1,accounts:{}},signal:{enabled:!1,accounts:{}},msteams:{enabled:!1,accounts:{}},googlechat:{enabled:!1,accounts:{}},line:{enabled:!1,accounts:{}},matrix:{enabled:!1,accounts:{}},responses:{enabled:!0,port:3004}}),models:B.optional().default(q),stt:A.optional().default({enabled:!1,provider:"openai-whisper","openai-whisper":{modelRef:"",model:"whisper-1",language:""},"local-whisper":{binaryPath:"whisper",model:"base"}}),tts:z.optional().default(Y),memory:K.optional().default(ee),agent:J.optional().default(te),cron:ne.optional().default(le),nostromo:re.optional().default(se),browser:p.optional().default({enabled:!1,controlPort:3002,headless:!1,noSandbox:!1,attachOnly:!1,remoteCdpTimeoutMs:1500,profiles:{default:{cdpPort:9222,color:"#FF4500"}}})});function ue(e){const t=function(e){const t=new Set,o=/\$\{([^}]+)\}/g;let a;for(;null!==(a=o.exec(e));)t.add(a[1]);return Array.from(t)}(e),o=t.filter(e=>!process.env[e]);o.length>0&&b.warn(`Missing environment variables referenced in config: ${o.join(", ")}. Add them to .env or export them before starting.`)}function de(e){if("string"==typeof e)return e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(Array.isArray(e))return e.map(de);if(null!==e&&"object"==typeof e){const t={};for(const[o,a]of Object.entries(e))t[o]=de(a);return t}return e}const ce={channels:{responses:{enabled:!0,port:3004}},stt:{enabled:!1},tts:Y,memory:ee,agent:{...te,permissionMode:"bypassPermissions",allowedTools:["Read","Grep","Bash","WebSearch","Glob","Write","Edit","WebFetch","Task","Skill"]},cron:le,nostromo:se};export function loadConfig(n){const l=n??r(process.cwd(),"config.yaml"),i=r(process.cwd(),".env");if(o(i)&&(u({path:i}),b.info(`Loaded .env from ${i}`)),!o(l)){const e="# GrabMeABeer Configuration\n# Configure channels and settings via Nostromo: http://localhost:3001\n\n"+c({gmabPath:"~/gmab",...ce});t(l,e,"utf-8")}const f=e(l,"utf-8");ue(f);const p=de(d(f)),m=ie.parse(p);if(h=process.env.GMAB_PATH?g(process.env.GMAB_PATH):g(m.gmabPath),y=s(h,"data"),a(y,{recursive:!0}),!m.timezone){m.timezone=Intl.DateTimeFormat().resolvedOptions().timeZone;try{const o=d(e(l,"utf-8"))??{};o.timezone=m.timezone,t(l,c(o),"utf-8"),b.info(`Timezone auto-detected and saved: ${m.timezone}`)}catch(e){}}return function(e){a(y,{recursive:!0});const t=process.env.WORKSPACE_PATH??e.agent.workspacePath;e.agent.workspacePath=r(g(t)),a(e.agent.workspacePath,{recursive:!0});const o=s(y,"cron");a(o,{recursive:!0});const n=e.cron.storePath.trim()?r(g(e.cron.storePath)):s(o,"jobs.json");return{...e,gmabPath:h,dataDir:y,dbPath:s(y,"core.db"),memoryDir:s(y,"memory"),cronStorePath:n}}(m)}export function loadRawConfig(t){const a=t??r(process.cwd(),"config.yaml");if(!o(a))return{};const n=e(a,"utf-8");return d(n)??{}}export function resolveModelEntry(e,t){if(!t)return;const o=e.models;if(!o?.length)return;const a=t.indexOf(":");if(a>=0){const e=t.substring(0,a),n=t.substring(a+1);return o.find(t=>t.name===e&&t.id===n)}return o.find(e=>e.name===t)??o.find(e=>e.id===t)}export function modelRefName(e){if(!e)return e;const t=e.indexOf(":");return t>=0?e.substring(0,t):e}export function resolveModelId(e,t){if(!t)return t;const o=resolveModelEntry(e,t);return o?o.id:t}
|
|
1
|
+
import{readFileSync as e,writeFileSync as t,existsSync as a,mkdirSync as o,renameSync as n,unlinkSync as l}from"node:fs";import{resolve as r,join as s}from"node:path";import{homedir as i}from"node:os";import{config as u}from"dotenv";import{parse as d,stringify as c}from"yaml";import{z as f}from"zod";import{BrowserConfigSchema as p}from"@hera-al/browser-server/config";import{createLogger as m}from"./utils/logger.js";const b=m("Config");function g(e){return"~"===e||e.startsWith("~/")?e.replace("~",i()):e}let h=g(process.env.GMAB_PATH??"~/gmab"),y=s(h,"data");export function getGmabPath(){return h}export function getDataDir(){return y}export function getNostromoKeyPath(){return s(y,".nostromo-key")}export function backupConfig(o){if(a(o))try{const r=e=>`${o}.backup${e}`;a(r(1))&&l(r(1));for(let e=2;e<=5;e++)a(r(e))&&n(r(e),r(e-1));t(r(5),e(o)),b.debug(`Config backup created: ${r(5)}`)}catch(e){b.warn(`Failed to create config backup: ${e}`)}}const v=f.record(f.string(),f.any()),x=f.object({reactions:f.boolean().default(!0),sendMessage:f.boolean().default(!0),editMessage:f.boolean().default(!0),deleteMessage:f.boolean().default(!0),sticker:f.boolean().default(!1),createForumTopic:f.boolean().default(!1)}),w=f.object({maxAttempts:f.number().default(3),baseDelayMs:f.number().default(1e3),maxDelayMs:f.number().default(3e4)}),M=f.enum(["off","dm","group","all","allowlist"]),k=f.object({botToken:f.string(),dmPolicy:f.enum(["open","token","allowlist"]).default("allowlist"),allowFrom:f.array(f.union([f.string(),f.number()])).default([]),name:f.string().optional(),reactionLevel:f.enum(["off","ack","minimal","extensive"]).default("ack"),reactionNotifications:f.enum(["off","own","all"]).default("off"),inlineButtonsScope:M.optional(),textChunkLimit:f.number().default(4e3),streamMode:f.enum(["off","partial","block"]).default("partial"),linkPreview:f.boolean().default(!0),actions:x.optional(),retry:w.optional(),timeoutSeconds:f.number().optional(),proxy:f.string().optional()}),j=f.object({enabled:f.boolean().default(!1),accounts:f.record(f.string(),k).default({})}),P=f.object({enabled:f.boolean().default(!1),accounts:v.default({})}),R=f.object({enabled:f.boolean().default(!0),port:f.number().default(3004)}),C=f.object({telegram:j.optional().default({enabled:!1,accounts:{}}),whatsapp:P.optional().default({enabled:!1,accounts:{}}),discord:P.optional().default({enabled:!1,accounts:{}}),slack:P.optional().default({enabled:!1,accounts:{}}),signal:P.optional().default({enabled:!1,accounts:{}}),msteams:P.optional().default({enabled:!1,accounts:{}}),googlechat:P.optional().default({enabled:!1,accounts:{}}),line:P.optional().default({enabled:!1,accounts:{}}),matrix:P.optional().default({enabled:!1,accounts:{}}),responses:R.optional().default({enabled:!0,port:3e3})}),S=f.object({modelRef:f.string().default(""),model:f.string().default("whisper-1"),language:f.string().default("")}),T=f.object({binaryPath:f.string().default("whisper"),model:f.string().default("base")}),A=f.object({enabled:f.boolean().default(!1),provider:f.string().default("openai-whisper"),"openai-whisper":S.optional().default({modelRef:"",model:"whisper-1",language:""}),"local-whisper":T.optional().default({binaryPath:"whisper",model:"base"})}),D=f.object({voice:f.string().default("en-US-MichelleNeural"),lang:f.string().default("en-US"),outputFormat:f.string().default("audio-24khz-48kbitrate-mono-mp3")}),I=f.object({modelRef:f.string().default(""),model:f.string().default("gpt-4o-mini-tts"),voice:f.string().default("alloy")}),U=f.object({modelRef:f.string().default(""),voiceId:f.string().default("pMsXgVXv3BLzUgSXRplE"),modelId:f.string().default("eleven_multilingual_v2")}),z=f.object({enabled:f.boolean().default(!1),provider:f.enum(["edge","openai","elevenlabs"]).default("openai"),maxTextLength:f.number().default(4096),timeoutMs:f.number().default(3e4),edge:D.optional().default({voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"}),openai:I.optional().default({modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"}),elevenlabs:U.optional().default({modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"})}),L=f.object({enabled:f.boolean().default(!1),embeddingModel:f.string().default("text-embedding-3-small"),embeddingDimensions:f.number().default(1536),modelRef:f.string().default(""),prefixQuery:f.string().default(""),prefixDocument:f.string().default(""),updateDebounceMs:f.number().default(3e3),embedIntervalMs:f.number().default(3e5),maxResults:f.number().default(6),maxSnippetChars:f.number().default(700),maxInjectedChars:f.number().default(4e3),rrfK:f.number().default(60)}),W={enabled:!1,embeddingModel:"text-embedding-3-small",embeddingDimensions:1536,modelRef:"",prefixQuery:"",prefixDocument:"",updateDebounceMs:3e3,embedIntervalMs:3e5,maxResults:6,maxSnippetChars:700,maxInjectedChars:4e3,rrfK:60},E=f.object({enabled:f.boolean().default(!0),model:f.string().default("")}),F={enabled:!0,model:""},K=f.object({enabled:f.boolean().default(!0),recallStrategy:f.enum(["builtin-only","search"]).default("builtin-only"),search:L.optional().default(W),l0:E.optional().default(F)}),q=f.object({command:f.string(),args:f.array(f.string()).optional(),env:f.record(f.string(),f.string()).optional()}).passthrough(),G=f.object({id:f.string(),name:f.string(),types:f.array(f.enum(["internal","external","env-var"])).optional().default(["external"]),proxy:f.enum(["not-used","direct","proxied"]).optional().default("not-used"),fastUrl:f.string().optional().default(""),fastProxyApiKey:f.string().optional().default(""),apiKey:f.string().optional().default(""),baseURL:f.string().optional().default(""),useEnvVar:f.string().optional().default(""),contextWindow:f.number().optional().default(2e5),costInput:f.number().optional().default(0),costOutput:f.number().optional().default(0),costCacheRead:f.number().optional().default(0),costCacheWrite:f.number().optional().default(0)}),O=[{id:"claude-opus-4-6",name:"Claude Opus",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-sonnet-4-6",name:"Claude Sonnet",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0}],_=f.array(G).default(O),B=f.object({name:f.string(),path:f.string(),description:f.string().default(""),enabled:f.boolean().default(!1)}),N=f.object({name:f.string(),description:f.string(),prompt:f.string(),model:f.enum(["sonnet","opus","haiku","inherit"]).default("inherit"),tools:f.array(f.string()).default(["Read","Write","Edit","Glob","Grep","WebSearch","WebFetch"]),expandContext:f.boolean().default(!1),enabled:f.boolean().default(!1)}),X=f.object({type:f.enum(["claudecode","pi"]).default("claudecode"),piModelRef:f.string().default("")}).passthrough(),$={type:"claudecode",piModelRef:""},V=f.object({enabled:f.boolean().default(!1),modelRefs:f.array(f.string()).default([]),rollingMemoryModel:f.string().default("")}),H={enabled:!1,modelRefs:[],rollingMemoryModel:""},Q=f.object({maxAttempts:f.number().default(5),baseDelayMs:f.number().default(2e3),maxDelayMs:f.number().default(3e4)}),Z={maxAttempts:5,baseDelayMs:2e3,maxDelayMs:3e4},J=f.object({model:f.string().default("claude-opus-4-6"),mainFallback:f.string().default(""),engine:X.optional().default($),picoAgent:V.optional().default(H),maxTurns:f.number().default(50),permissionMode:f.string().default("bypassPermissions"),sessionTTL:f.number().default(3600),queueMode:f.enum(["queue","collect","steer"]).default("steer"),queueDebounceMs:f.number().default(1500),queueCap:f.number().default(20),queueDropPolicy:f.enum(["old","new","summarize"]).default("summarize"),allowedTools:f.array(f.string()).default([]),disallowedTools:f.array(f.string()).default([]),mcpServers:f.record(f.string(),q).default({}),workspacePath:f.string().default("./workspace"),builtinCoderSkill:f.boolean().default(!1),settingSources:f.enum(["nothing","user","project","both"]).default("project"),customSubAgents:f.array(N).default([]),plugins:f.array(B).default([]),inflightTyping:f.boolean().default(!0),autoApproveTools:f.boolean().default(!0),autoRenew:f.number().default(0),apiRetry:Q.optional().default(Z),skillNudge:f.object({enabled:f.boolean().default(!0),threshold:f.number().default(10)}).default({enabled:!0,threshold:10}),qualityGate:f.object({enabled:f.boolean().default(!1)}).default({enabled:!1})}),Y={enabled:!1,provider:"openai",maxTextLength:4096,timeoutMs:3e4,edge:{voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"},openai:{modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"},elevenlabs:{modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"}},ee={enabled:!0,recallStrategy:"builtin-only",dir:"",search:W,l0:F},te={model:"claude-opus-4-6",mainFallback:"",engine:$,picoAgent:H,maxTurns:50,permissionMode:"bypassPermissions",sessionTTL:3600,queueMode:"steer",queueDebounceMs:1500,queueCap:20,queueDropPolicy:"summarize",allowedTools:[],disallowedTools:[],mcpServers:{},workspacePath:"./workspace",builtinCoderSkill:!1,settingSources:"project",customSubAgents:[],plugins:[],inflightTyping:!0,autoApproveTools:!0,autoRenew:0,apiRetry:Z,skillNudge:{enabled:!0,threshold:10},qualityGate:{enabled:!1}},ae=f.object({enabled:f.boolean().default(!1),every:f.number().default(18e5),channel:f.string().default(""),chatId:f.string().default(""),message:f.string().default(""),ackMaxChars:f.number().default(300)}),oe={enabled:!1,every:18e5,channel:"",chatId:"",message:"",ackMaxChars:300},ne=f.object({enabled:f.boolean().default(!0),isolated:f.boolean().default(!0),broadcastEvents:f.boolean().default(!1),storePath:f.string().default(""),heartbeat:ae.optional().default(oe)}),le={enabled:!0,isolated:!0,broadcastEvents:!1,storePath:"",heartbeat:oe},re=f.object({enabled:f.boolean().default(!0),port:f.number().default(3001),basePath:f.string().default("/nostromo"),configCheckInterval:f.number().default(5),autoRestart:f.boolean().default(!0)}),se={enabled:!0,port:3001,basePath:"/nostromo",configCheckInterval:5,autoRestart:!0},ie=f.object({gmabPath:f.string().optional().default("~/gmab"),host:f.string().optional().default("127.0.0.1"),logLevel:f.enum(["debug","info","warn","error"]).optional().default("info"),verboseDebugLogs:f.boolean().optional().default(!0),timezone:f.string().optional().default(""),fastProxyUrl:f.string().optional().default("http://localhost:4181"),channels:C.optional().default({telegram:{enabled:!1,accounts:{}},whatsapp:{enabled:!1,accounts:{}},discord:{enabled:!1,accounts:{}},slack:{enabled:!1,accounts:{}},signal:{enabled:!1,accounts:{}},msteams:{enabled:!1,accounts:{}},googlechat:{enabled:!1,accounts:{}},line:{enabled:!1,accounts:{}},matrix:{enabled:!1,accounts:{}},responses:{enabled:!0,port:3004}}),models:_.optional().default(O),stt:A.optional().default({enabled:!1,provider:"openai-whisper","openai-whisper":{modelRef:"",model:"whisper-1",language:""},"local-whisper":{binaryPath:"whisper",model:"base"}}),tts:z.optional().default(Y),memory:K.optional().default(ee),agent:J.optional().default(te),cron:ne.optional().default(le),nostromo:re.optional().default(se),browser:p.optional().default({enabled:!1,controlPort:3002,headless:!1,noSandbox:!1,attachOnly:!1,remoteCdpTimeoutMs:1500,profiles:{default:{cdpPort:9222,color:"#FF4500"}}})});function ue(e){const t=function(e){const t=new Set,a=/\$\{([^}]+)\}/g;let o;for(;null!==(o=a.exec(e));)t.add(o[1]);return Array.from(t)}(e),a=t.filter(e=>!process.env[e]);a.length>0&&b.warn(`Missing environment variables referenced in config: ${a.join(", ")}. Add them to .env or export them before starting.`)}function de(e){if("string"==typeof e)return e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(Array.isArray(e))return e.map(de);if(null!==e&&"object"==typeof e){const t={};for(const[a,o]of Object.entries(e))t[a]=de(o);return t}return e}const ce={channels:{responses:{enabled:!0,port:3004}},stt:{enabled:!1},tts:Y,memory:ee,agent:{...te,permissionMode:"bypassPermissions",allowedTools:["Read","Grep","Bash","WebSearch","Glob","Write","Edit","WebFetch","Task","Skill"]},cron:le,nostromo:se};export function loadConfig(n){const l=n??r(process.cwd(),"config.yaml"),i=r(process.cwd(),".env");if(a(i)&&(u({path:i}),b.info(`Loaded .env from ${i}`)),!a(l)){const e="# GrabMeABeer Configuration\n# Configure channels and settings via Nostromo: http://localhost:3001\n\n"+c({gmabPath:"~/gmab",...ce});t(l,e,"utf-8")}const f=e(l,"utf-8");ue(f);const p=de(d(f)),m=ie.parse(p);if(h=process.env.GMAB_PATH?g(process.env.GMAB_PATH):g(m.gmabPath),y=s(h,"data"),o(y,{recursive:!0}),!m.timezone){m.timezone=Intl.DateTimeFormat().resolvedOptions().timeZone;try{const a=d(e(l,"utf-8"))??{};a.timezone=m.timezone,t(l,c(a),"utf-8"),b.info(`Timezone auto-detected and saved: ${m.timezone}`)}catch(e){}}return function(e){o(y,{recursive:!0});const t=process.env.WORKSPACE_PATH??e.agent.workspacePath;e.agent.workspacePath=r(g(t)),o(e.agent.workspacePath,{recursive:!0});const a=s(y,"cron");o(a,{recursive:!0});const n=e.cron.storePath.trim()?r(g(e.cron.storePath)):s(a,"jobs.json");return{...e,gmabPath:h,dataDir:y,dbPath:s(y,"core.db"),memoryDir:s(y,"memory"),cronStorePath:n}}(m)}export function loadRawConfig(t){const o=t??r(process.cwd(),"config.yaml");if(!a(o))return{};const n=e(o,"utf-8");return d(n)??{}}export function resolveModelEntry(e,t){if(!t)return;const a=e.models;if(!a?.length)return;const o=t.indexOf(":");if(o>=0){const e=t.substring(0,o),n=t.substring(o+1);return a.find(t=>t.name===e&&t.id===n)}return a.find(e=>e.name===t)??a.find(e=>e.id===t)}export function modelRefName(e){if(!e)return e;const t=e.indexOf(":");return t>=0?e.substring(0,t):e}export function resolveModelId(e,t){if(!t)return t;const a=resolveModelEntry(e,t);return a?a.id:t}
|