openvisio-agent 0.7.1 → 0.8.1
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 +32 -9
- package/src/ws.mjs +4 -0
package/package.json
CHANGED
package/src/watch.mjs
CHANGED
|
@@ -34,7 +34,7 @@ const REPLY_DISCIPLINE = [
|
|
|
34
34
|
|
|
35
35
|
// ── CHAT-ONLY agents (no --workdir): chat/ticket tools, no code surface. ──────
|
|
36
36
|
const CHAT_CHARTER = [
|
|
37
|
-
'YOU ARE a connected agent in an OpenVisio team, running in CHAT-ONLY mode. Your tools are the openvisio-team chat/ticket tools
|
|
37
|
+
'YOU ARE a connected agent in an OpenVisio team, running in CHAT-ONLY mode. Your tools are the openvisio-team chat/ticket tools — post_message, poll_inbox, comment_ticket, react_message (your runtime may namespace them, e.g. mcp__openvisio-team__post_message or openvisio-team_post_message — call whatever names appear in your tool list). You have NO file/Bash/git tools in this mode, so you cannot write code yourself.',
|
|
38
38
|
'WORK ETHIC — behave like a dependable teammate: never leave a promise dangling. Either ACT now (reply, or file a ticket) or say plainly you can\'t and offer to file a ticket / tag a coding agent who can. Never invent progress. Close the loop every cycle — the human should never have to remind you to circle back.',
|
|
39
39
|
'',
|
|
40
40
|
REPLY_DISCIPLINE,
|
|
@@ -43,7 +43,7 @@ const CHAT_CHARTER = [
|
|
|
43
43
|
const CYCLE = CHAT_CHARTER + '\n\nRun one OpenVisio autonomy cycle: call poll_inbox and handle mentions + follow-ups. Reply in 1-3 sentences, @mention people by their EXACT full name, at most one reply per channel. Then stop.'
|
|
44
44
|
const CYCLE_FAST = CHAT_CHARTER + '\n\n' + [
|
|
45
45
|
'New chat activity. Do EXACTLY ONE of these:',
|
|
46
|
-
' • IF a specific mention/message FOR YOU is given above: reply to THAT ONE message exactly once with
|
|
46
|
+
' • IF a specific mention/message FOR YOU is given above: reply to THAT ONE message exactly once with post_message, then STOP. Do NOT call poll_inbox and do NOT answer anything else this cycle — you already have the message; polling would make you re-answer it and double-post.',
|
|
47
47
|
' • IF NO specific mention is given above: call poll_inbox and reply only to items truly directed at YOU (a question to you, or a reply to your own message) — SKIP chatter aimed at someone else / another agent, ignore .tasks/.claimable, at most one reply per channel.',
|
|
48
48
|
'Post ONE message total for the thing you are answering — compose it fully, then send once. Never send a reply and then a "better" version; never repeat a reply you already sent. Be sure of your answer before sending. If asked for work you have no tool for, say so plainly and offer to file a ticket. 1-3 sentences, no summary. Then stop.',
|
|
49
49
|
].join('\n')
|
|
@@ -52,7 +52,7 @@ const CYCLE_FAST = CHAT_CHARTER + '\n\n' + [
|
|
|
52
52
|
// A stable "who you are / how you work" charter prepended to every code cycle.
|
|
53
53
|
const CODE_CHARTER = [
|
|
54
54
|
'YOU ARE a connected CODING agent in an OpenVisio team, running ON THE USER\'S LAPTOP. You have REAL tools — use them; do NOT claim you lack a capability without checking what you actually hold. Your toolbox:',
|
|
55
|
-
' • openvisio-team chat/ticket tools
|
|
55
|
+
' • openvisio-team chat/ticket tools — read mentions, post messages, comment on tickets, react (post_message / poll_inbox / comment_ticket / react_message; your runtime may namespace them — use whatever names appear in your tool list).',
|
|
56
56
|
' • Read / Grep / Glob / Edit / Write / MultiEdit — inspect AND change code.',
|
|
57
57
|
' • Bash — git (branch, commit, push a branch), gh (clone repos, open PRs), run tests/builds.',
|
|
58
58
|
'YOUR WORKSPACE: your working directory is a WORKSPACE ROOT that holds the org\'s repos as subfolders. You are NOT limited to one repo — you work across ALL of them from here. For any task: find the relevant repo under the workspace; if it isn\'t cloned yet, CLONE it (gh repo clone <org>/<repo>, or git clone <url>) and then work inside that subfolder. Never make the user tell you a path or clone things for you.',
|
|
@@ -82,7 +82,7 @@ const CODE_FULL = CODE_CHARTER + '\n\n' + [
|
|
|
82
82
|
|
|
83
83
|
const CODE_FAST = CODE_CHARTER + '\n\n' + [
|
|
84
84
|
'New chat activity. Do EXACTLY ONE of these:',
|
|
85
|
-
' • IF a specific mention/message FOR YOU is given above: reply to THAT ONE message exactly once with
|
|
85
|
+
' • IF a specific mention/message FOR YOU is given above: reply to THAT ONE message exactly once with post_message, then STOP. Do NOT call poll_inbox and do NOT answer anything else this cycle — polling would re-surface the same message and make you double-post.',
|
|
86
86
|
' • IF NO specific mention is given above: call poll_inbox and reply only to items directed at YOU (asks you something, or responds to your own message) — SKIP chatter aimed at someone else / another agent; at most one reply per channel.',
|
|
87
87
|
'If the message asks YOU for real CODE work, do the WHOLE job now — branch (checkout -B agent/<slug>), edit, run tests, commit, push your branch, open a PR (gh pr create), then reply once with the PR link and @mention the requester by exact full name. Never push to main, never --force, never merge.',
|
|
88
88
|
'Post ONE message for the thing you are answering — send it once; never a reply then a "better" version; never repeat a reply you already sent; be sure before you send. Do NOT promise and stop — finish and report in THIS cycle. 1-3 sentences, no summary. Then stop.',
|
|
@@ -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.
|
|
@@ -407,7 +420,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
407
420
|
// The Mastra bridge authenticates per-CALL, not per-connection: every
|
|
408
421
|
// openvisio-team tool needs agent_identifier + agent_api_key as arguments.
|
|
409
422
|
// Hand them to the model up front so it never shells around hunting for them.
|
|
410
|
-
const credNote = `AUTH: the openvisio-team
|
|
423
|
+
const credNote = `AUTH: the openvisio-team tools REQUIRE two arguments on EVERY call — agent_identifier: "${identifier}" and agent_api_key: "${apiKey}". Include BOTH on every openvisio-team tool call (post_message, poll_inbox, react_message, comment_ticket, get_marching_orders, …). These tools may appear in your tool list namespaced (mcp__openvisio-team__post_message on Claude Code, openvisio-team_post_message on opencode) — call whichever names you actually see. The credentials are given to you right here — do NOT hunt for them (no Bash/grep/cat/find, no reading memory); just call the tools with these exact values. (Bash/git/gh ARE for your code work — this rule is only about not searching for these keys.)`
|
|
411
424
|
|
|
412
425
|
// Higher rank wins when coalescing cycles requested while one is running.
|
|
413
426
|
const RANK = { fast: 0, intro: 1, sweep: 2, full: 3 }
|
|
@@ -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,8 +548,11 @@ 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
|
-
? `You were @mentioned in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". This mention is FOR YOU. Send EXACTLY ONE reply with
|
|
555
|
+
? `You were @mentioned in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". This mention is FOR YOU. Send EXACTLY ONE reply with 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
|
|
534
557
|
void drain('fast', ctx)
|
|
535
558
|
} else if (k === 'error') {
|
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()
|