grix-connector
Version:
Connect local AI coding agents (Claude, Codex, Gemini, Qwen, DeepSeek, Cursor, OpenCode, Pi, OpenHuman, Reasonix) to the Grix scheduling platform. Also serves as an OpenClaw plugin for Grix channel transport.
4 lines (3 loc) • 7.29 kB
JavaScript
import{log as c}from"../core/log/index.js";import{splitTextForAibotProtocol as g,AIBOT_PROTOCOL_MAX_RUNES as C}from"../core/protocol/protocol-text.js";import{isThinkingEventDropped as E,isToolEventDropped as k,normalizeRuntimeConfigSnapshot as S}from"./runtime-config.js";class d{handle;connectorRuntimeConfig;eventRuntimeConfigs=new Map;thinkingSeqMap=new Map;toolCardSeq=0;bufferedEventTexts=new Map;chunkSeqMap=new Map;rejectedEvents=new Set;stoppedEvents=new Set;streamCoalesce=new Map;streamCoalesceMs=T();static STREAM_COALESCE_MAX_RUNES=2048;constructor(e){this.connectorRuntimeConfig={...e}}bind(e){this.handle=e}markEventRejected(e){if(!e)return;this.rejectedEvents.add(e);const t=`${e}:`;for(const s of[...this.streamCoalesce.keys()])if(s.startsWith(t)){const n=this.streamCoalesce.get(s);n.timer&&clearTimeout(n.timer),this.streamCoalesce.delete(s)}c.info("send-ctrl",`event marked as stream-rejected, future chunks will be dropped event=${e}`)}markEventStopped(e){e&&(this.stoppedEvents.add(e),c.info("send-ctrl",`[stop-trace] event marked stopped; subsequent outbound will be flagged event=${e}`))}flagOutboundAfterStop(e,t,s,n,i,r){!e||!this.stoppedEvents.has(e)||c.warn("send-ctrl",`[stop-trace] OUTBOUND AFTER STOP ${s} event=${e} session=${t} seq=${n} is_finish=${i} len=${r}`)}getGlobalRuntimeConfig(){return this.connectorRuntimeConfig}setStreamCoalesceMs(e){this.streamCoalesceMs=Number.isFinite(e)&&e>0?Math.floor(e):0}captureEventRuntimeConfig(e){const t=S(e.connector_runtime_config)??this.connectorRuntimeConfig;this.eventRuntimeConfigs.set(e.event_id,{responseDelivery:t.responseDelivery,toolEvents:t.toolEvents,thinkingEvents:t.thinkingEvents})}resolveEventRuntimeConfig(e){if(e){const t=this.eventRuntimeConfigs.get(e);if(t)return t}return this.connectorRuntimeConfig}shouldDropToolDisplayEvent(e){return k(this.resolveEventRuntimeConfig(e))}shouldDropThinkingDisplayEvent(e){return E(this.resolveEventRuntimeConfig(e))}shouldDropCodexDisplayEvent(e,t){return this.shouldDropToolDisplayEvent(e)?t==="item/completed":!1}shouldDropAcpRawDisplayEvent(e,t){return!!(this.shouldDropToolDisplayEvent(e)&&(t==="tool_use"||t==="tool_result")||this.shouldDropThinkingDisplayEvent(e)&&t==="thinking")}sendReply(e,t,s,n,i,r){if(!r?.skipToolFilter&&this.shouldDropToolDisplayEvent(e)&&y(s,i)||e&&this.rejectedEvents.has(e))return;if(this.flagOutboundAfterStop(e,t,"reply",0,!1,(s||"").length),this.resolveEventRuntimeConfig(e).responseDelivery==="stream"&&!i){const o=g(s),u=e?`reply_stream_${e}`:`reply_stream_${Date.now()}`;if(o.length===0){this.handle.sendMsg({event_id:e,session_id:t,msg_type:1,content:s||" ",quoted_message_id:n||void 0});return}for(let a=0;a<o.length;a++){const h=a===o.length-1;this.sendStreamChunk(e,t,o[a],a+1,h,u,n)}return}this.handle.sendMsg({event_id:e,session_id:t,msg_type:1,content:s,quoted_message_id:n||void 0,...i?{extra:i}:{}})}sendStreamChunk(e,t,s,n,i,r,l){if(e&&this.rejectedEvents.has(e))return;if(this.flagOutboundAfterStop(e,t,"stream_chunk",n,i,(s||"").length),this.finalizeThinking(e,t),this.resolveEventRuntimeConfig(e).responseDelivery==="single_message"&&e){this.bufferStreamChunk(e,t,s,i,l);return}if(this.streamCoalesceMs<=0){this.emitStreamChunkParts(e,t,s,n,i,r,l);return}this.coalesceStreamChunk(e,t,s,i,r,l)}coalesceStreamChunk(e,t,s,n,i,r){if(!s&&!n)return;const l=`${e}:${i??"default"}`;let o=this.streamCoalesce.get(l);if(o||(o={eventId:e,sessionId:t,clientMsgId:i,quotedMessageId:r,text:"",flushedOnce:!1},this.streamCoalesce.set(l,o)),s&&(o.text+=s),o.sessionId=t,i!==void 0&&(o.clientMsgId=i),!o.quotedMessageId&&r&&(o.quotedMessageId=r),n){this.flushCoalesce(l,!0);return}if(!o.flushedOnce||o.text.length>=d.STREAM_COALESCE_MAX_RUNES){this.flushCoalesce(l,!1);return}o.timer||(o.timer=setTimeout(()=>this.flushCoalesce(l,!1),this.streamCoalesceMs))}flushCoalesce(e,t){const s=this.streamCoalesce.get(e);if(!s)return;s.timer&&(clearTimeout(s.timer),s.timer=void 0);const n=s.text;s.text="",s.flushedOnce=!0,t&&this.streamCoalesce.delete(e),!(!n&&!t)&&this.emitStreamChunkParts(s.eventId,s.sessionId,n,0,t,s.clientMsgId,s.quotedMessageId)}flushCoalesceForEvent(e){const t=`${e}:`;for(const s of[...this.streamCoalesce.keys()])s.startsWith(t)&&(this.flushCoalesce(s,!1),this.streamCoalesce.delete(s))}emitStreamChunkParts(e,t,s,n,i,r,l){const o=s||(i?`
`:"");if(!o&&!i)return;const u=g(o,C);u.length===0&&u.push(o||`
`);const a=`${e}:${r??"default"}`;for(let h=0;h<u.length;h++){const _=h===u.length-1,m=this.chunkSeqMap.get(a)??0,p=h===0&&n>m?n:m+1;this.chunkSeqMap.set(a,p),this.handle.sendStreamChunk({event_id:e,session_id:t,delta_content:u[h],chunk_seq:p,is_finish:_&&i,...r?{client_msg_id:r}:{},...l?{quoted_message_id:l}:{}})}}sendEventResult(e,t,s,n){this.finalizeThinking(e,""),this.flushCoalesceForEvent(e),this.flushBufferedStreamText(e),this.handle.sendEventResult({event_id:e,status:t,...s?{msg:s}:{},...n?{code:n}:{},updated_at:Date.now()}),this.eventRuntimeConfigs.delete(e),this.rejectedEvents.delete(e),this.stoppedEvents.delete(e)&&c.info("send-ctrl",`[stop-trace] event_result(${t}) cleared stop flag event=${e}`);for(const i of this.chunkSeqMap.keys())i.startsWith(`${e}:`)&&this.chunkSeqMap.delete(i)}sendThinking(e,t,s){if(this.shouldDropThinkingDisplayEvent(e)||e&&this.rejectedEvents.has(e))return;this.flagOutboundAfterStop(e,t,"thinking",this.thinkingSeqMap.get(e)??0,!1,(s||"").length);const n=(this.thinkingSeqMap.get(e)??0)+1;this.thinkingSeqMap.set(e,n),this.handle.sendStreamChunk({event_id:e,session_id:t,delta_content:s,chunk_seq:n,is_finish:!1,client_msg_id:`${e}_thinking`,is_thinking:!0})}sendToolExecutionCard(e,t,s,n){if(this.shouldDropToolDisplayEvent(e)){c.info("send-ctrl",`[tool-card] DROP event=${e} session=${t} summary=${s.summaryText}`);return}const r={summary_text:s.summaryText};s.detailText&&(r.detail_text=s.detailText),c.info("send-ctrl",`[tool-card] SEND event=${e} session=${t} summary=${s.summaryText}`),this.handle.sendMsg({event_id:e,session_id:t,client_msg_id:n??`tool_${++this.toolCardSeq}_${Date.now()}`,msg_type:1,content:"",extra:{channel_data:{grix:{toolExecution:r}},agent_api_origin:!0}})}finalizeThinking(e,t){const s=this.thinkingSeqMap.get(e);s!==void 0&&(this.thinkingSeqMap.delete(e),t&&(e&&this.rejectedEvents.has(e)||this.handle.sendStreamChunk({event_id:e,session_id:t,delta_content:"",chunk_seq:s+1,is_finish:!0,client_msg_id:`${e}_thinking`,is_thinking:!0})))}bufferOnly(e,t,s,n,i){this.bufferStreamChunk(e,t,s,n,i)}bufferStreamChunk(e,t,s,n,i){const r=this.bufferedEventTexts.get(e);r?(s&&(r.text+=s),!r.quotedMessageId&&i&&(r.quotedMessageId=i)):this.bufferedEventTexts.set(e,{sessionId:t,text:s??"",quotedMessageId:i||void 0}),n&&this.flushBufferedStreamText(e)}flushBufferedStreamText(e){const t=this.bufferedEventTexts.get(e);t&&(this.bufferedEventTexts.delete(e),t.text&&this.handle.sendMsg({event_id:e,session_id:t.sessionId,msg_type:1,content:t.text,...t.quotedMessageId?{quoted_message_id:t.quotedMessageId}:{}}))}}function T(){const f=process.env.GRIX_STREAM_COALESCE_MS;if(f===void 0||f==="")return 500;const e=Number(f);return Number.isFinite(e)&&e>=0?Math.floor(e):500}function y(f,e){return e&&typeof e=="object"&&e.channel_data?.grix?.toolExecution?!0:/^\[tool\]/.test(f)||/^\[tool result\]/.test(f)}export{d as SendController};