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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openvisio-agent",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Connect your coding agent (Claude Code) to an OpenVisio team — MCP tools + optional autonomy — in one command. No shell scripts.",
5
5
  "type": "module",
6
6
  "bin": {
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
- const { runCycle, canCode } = createCycleRunner({ claude, agent, mcpUrl, mcpHeaders: { 'x-agent-api-key': apiKey, 'x-agent-identifier': identifier }, cfgKey: identifier, mcpConfig, workdir, log, debug, model })
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()