openvisio-agent 0.7.1 → 0.8.0
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/package.json +1 -1
- package/src/watch.mjs +26 -3
- package/src/ws.mjs +4 -0
package/package.json
CHANGED
package/src/watch.mjs
CHANGED
|
@@ -276,7 +276,7 @@ function createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, ma
|
|
|
276
276
|
// ── Claude Code warm-session cycle runner (shared by the REST + WS loops) ─────
|
|
277
277
|
// One persistent stream-json session, poked with a prompt per cycle. Recycled
|
|
278
278
|
// after MAX_TURNS or SESSION_IDLE_MS. Returns { runCycle, canCode }.
|
|
279
|
-
function createCycleRunner({ claude, agent, mcpUrl, mcpHeaders, cfgKey, mcpConfig, workdir, log, debug, model }) {
|
|
279
|
+
function createCycleRunner({ claude, agent, mcpUrl, mcpHeaders, cfgKey, mcpConfig, workdir, log, debug, model, onTool }) {
|
|
280
280
|
const canCode = !!workdir
|
|
281
281
|
const maxCycleMs = canCode ? MAX_CODE_CYCLE_MS : MAX_CYCLE_MS
|
|
282
282
|
// opencode drives cycles differently — a headless `opencode run` per cycle rather
|
|
@@ -328,6 +328,10 @@ function createCycleRunner({ claude, agent, mcpUrl, mcpHeaders, cfgKey, mcpConfi
|
|
|
328
328
|
if (!line.trim()) continue
|
|
329
329
|
let o; try { o = JSON.parse(line) } catch { continue }
|
|
330
330
|
if (debug) logEvent(o)
|
|
331
|
+
// Surface tool calls (e.g. post_message) so the loop can emit a live status.
|
|
332
|
+
if (onTool && o.type === 'assistant' && o.message && Array.isArray(o.message.content)) {
|
|
333
|
+
for (const b of o.message.content) if (b.type === 'tool_use' && b.name) { try { onTool(b.name) } catch { /* status is best-effort */ } }
|
|
334
|
+
}
|
|
331
335
|
if (o.type === 'result') { log('cycle done (' + (o.subtype || 'ok') + (o.is_error ? ' · ERROR' : '') + ')'); settleTurn(o) }
|
|
332
336
|
}
|
|
333
337
|
})
|
|
@@ -378,7 +382,17 @@ function createCycleRunner({ claude, agent, mcpUrl, mcpHeaders, cfgKey, mcpConfi
|
|
|
378
382
|
// stack up N sessions. Plus a one-time intro on first connect and a daily catch-up sweep.
|
|
379
383
|
function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConfig, mcpUrl, workdir, model, chatModel, debug }) {
|
|
380
384
|
const log = (m) => process.stdout.write('[ws ' + new Date().toISOString() + '] ' + m + '\n')
|
|
381
|
-
|
|
385
|
+
let handle = null
|
|
386
|
+
// Channels the agent is actively working in this cycle — drives the live
|
|
387
|
+
// agent:status broadcast (thinking → working → typing → done).
|
|
388
|
+
const statusTargets = new Set()
|
|
389
|
+
const emitStatus = (state) => { for (const c of statusTargets) { try { handle && handle.sendStatus(c, state) } catch { /* best-effort */ } } }
|
|
390
|
+
const { runCycle, canCode } = createCycleRunner({
|
|
391
|
+
claude, agent, mcpUrl, mcpHeaders: { 'x-agent-api-key': apiKey, 'x-agent-identifier': identifier },
|
|
392
|
+
cfgKey: identifier, mcpConfig, workdir, log, debug, model,
|
|
393
|
+
// The moment the agent calls post_message it is about to speak → "typing".
|
|
394
|
+
onTool: (name) => { if (/post_message/.test(name)) emitStatus('typing') },
|
|
395
|
+
})
|
|
382
396
|
const fullPrompt = canCode ? CODE_FULL : CYCLE
|
|
383
397
|
const fastPrompt = canCode ? CODE_FAST : CYCLE_FAST
|
|
384
398
|
// Live model state — changeable at runtime by the in-chat `/model` command.
|
|
@@ -389,7 +403,6 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
389
403
|
|
|
390
404
|
let busy = false
|
|
391
405
|
let queued = null // 'full' | 'fast' | 'sweep' | 'intro' — a cycle requested while one was running
|
|
392
|
-
let handle = null
|
|
393
406
|
// Tasks we've already reacted to (keyed task-id:agent — so a REASSIGNMENT to a
|
|
394
407
|
// different agent re-triggers), so a noisy stream of task:updated events doesn't
|
|
395
408
|
// re-acknowledge the same assignment.
|
|
@@ -423,9 +436,16 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
423
436
|
// work (full/sweep) uses the main model.
|
|
424
437
|
const useModel = (kind === 'fast' || kind === 'intro') ? liteModel : codeModel
|
|
425
438
|
log('running ' + kind + ' cycle…' + (ctx.length ? ' (' + ctx.length + ' event' + (ctx.length === 1 ? '' : 's') + ')' : '') + (useModel ? ' [' + useModel + ']' : ''))
|
|
439
|
+
// Live status: "working" now + a heartbeat so the UI (and its TTL) stays lit
|
|
440
|
+
// through a long cycle; onTool flips it to "typing" when post_message fires.
|
|
441
|
+
const targets = [...statusTargets]
|
|
442
|
+
emitStatus('working')
|
|
443
|
+
const heartbeat = targets.length ? setInterval(() => emitStatus('working'), 9000) : null
|
|
426
444
|
try {
|
|
427
445
|
await runCycle(prompt, useModel)
|
|
428
446
|
} finally {
|
|
447
|
+
if (heartbeat) clearInterval(heartbeat)
|
|
448
|
+
for (const c of targets) { try { handle && handle.sendStatus(c, 'done') } catch { /* noop */ } statusTargets.delete(c) }
|
|
429
449
|
busy = false
|
|
430
450
|
if (queued || pending.length) { const next = queued || 'fast'; queued = null; void drain(next) }
|
|
431
451
|
}
|
|
@@ -528,6 +548,9 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
528
548
|
return
|
|
529
549
|
}
|
|
530
550
|
log('agent:mention in channel ' + (cid != null ? cid : '?') + (threadRoot != null ? ' (thread ' + threadRoot + ')' : ''))
|
|
551
|
+
// Light up the live status the instant we pick this up (thinking → the cycle
|
|
552
|
+
// takes it to working → typing → done).
|
|
553
|
+
if (cid != null) { statusTargets.add(cid); try { handle && handle.sendStatus(cid, 'thinking') } catch { /* best-effort */ } }
|
|
531
554
|
const ctx = cid != null
|
|
532
555
|
? `You were @mentioned in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". This mention is FOR YOU. Send EXACTLY ONE reply with mcp__openvisio-team__post_message — arguments: channel_id ${cid}${threadRoot != null ? `, parent_id ${threadRoot} (reply IN THAT THREAD, do not start a new top-level message)` : ''}, plus agent_identifier + agent_api_key from the AUTH line above, and a 1-3 sentence reply. Compose the whole answer, then post it ONCE — do NOT post a first reply and then a revised/"better" one. FIRST read the recent messages in this thread: if you already answered this, or another agent was the one addressed, do NOT post at all. Be sure of your answer before sending.${who ? ` To @mention them back, write their EXACT full name "@${who}" (a mention only links when the name matches exactly — "@${who.split(' ')[0]}" alone will NOT).` : ''} You ALREADY have the message here — do NOT poll_inbox, and after your single reply, STOP.`
|
|
533
556
|
: undefined
|
package/src/ws.mjs
CHANGED
|
@@ -100,6 +100,10 @@ export function connectAgentWs({ wsUrl, apiKey, identifier, onEvent, log }) {
|
|
|
100
100
|
send(obj) { try { if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj)) } catch { /* closing */ } },
|
|
101
101
|
/** Show the agent as typing in a channel (docs/WEBSOCKET.md `{type:'typing'}`). */
|
|
102
102
|
sendTyping(channelId) { api.send({ type: 'typing', channel_id: channelId }) },
|
|
103
|
+
/** Broadcast a richer live activity state for the agent in a channel — the
|
|
104
|
+
* backend fans it out as `agent:status` (see docs/AGENT_STATUS.md). state is
|
|
105
|
+
* one of thinking | working | typing | done. */
|
|
106
|
+
sendStatus(channelId, state, detail) { api.send({ type: 'agent_status', channel_id: channelId, state, ...(detail ? { detail: String(detail).slice(0, 120) } : {}) }) },
|
|
103
107
|
close() {
|
|
104
108
|
closed = true
|
|
105
109
|
clearKeepalive()
|