openvisio-agent 0.8.0 → 0.9.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 +45 -24
package/package.json
CHANGED
package/src/watch.mjs
CHANGED
|
@@ -34,16 +34,19 @@ 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,
|
|
41
41
|
].join('\n')
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// NOTE: the CHARTER (who you are + reply discipline) is NOT prepended here — it is
|
|
44
|
+
// passed ONCE as the session's system prompt (cacheable, not re-billed every cycle).
|
|
45
|
+
// These bases are the small per-cycle instructions only. See createCycleRunner.
|
|
46
|
+
const CYCLE = 'Run 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.'
|
|
47
|
+
const CYCLE_FAST = [
|
|
45
48
|
'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
|
|
49
|
+
' • 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
50
|
' • 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
51
|
'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
52
|
].join('\n')
|
|
@@ -52,7 +55,7 @@ const CYCLE_FAST = CHAT_CHARTER + '\n\n' + [
|
|
|
52
55
|
// A stable "who you are / how you work" charter prepended to every code cycle.
|
|
53
56
|
const CODE_CHARTER = [
|
|
54
57
|
'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
|
|
58
|
+
' • 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
59
|
' • Read / Grep / Glob / Edit / Write / MultiEdit — inspect AND change code.',
|
|
57
60
|
' • Bash — git (branch, commit, push a branch), gh (clone repos, open PRs), run tests/builds.',
|
|
58
61
|
'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.',
|
|
@@ -67,7 +70,7 @@ const CODE_CHARTER = [
|
|
|
67
70
|
REPLY_DISCIPLINE,
|
|
68
71
|
].join('\n')
|
|
69
72
|
|
|
70
|
-
const CODE_FULL =
|
|
73
|
+
const CODE_FULL = [
|
|
71
74
|
'THIS CYCLE: call get_marching_orders and poll_inbox to see assigned tickets + mentions, then act on them.',
|
|
72
75
|
'ACKNOWLEDGE ONCE: for a task assigned to you that you have NOT already acknowledged, post a SINGLE one-line comment_ticket ("On it — picking this up now") before you start. First check the ticket/thread — if you already acknowledged it on an earlier cycle, skip this and just keep working. Then report only when you have the result.',
|
|
73
76
|
'For real code work (an assigned ticket, or a mention asking for changes), run the full flow end-to-end:',
|
|
@@ -80,9 +83,9 @@ const CODE_FULL = CODE_CHARTER + '\n\n' + [
|
|
|
80
83
|
'Bash is for git / gh / tests / clone ONLY — never to hunt for credentials (they are given to you above).',
|
|
81
84
|
].join('\n')
|
|
82
85
|
|
|
83
|
-
const CODE_FAST =
|
|
86
|
+
const CODE_FAST = [
|
|
84
87
|
'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
|
|
88
|
+
' • 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
89
|
' • 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
90
|
'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
91
|
'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.',
|
|
@@ -122,7 +125,12 @@ const DENY_TOOLS = [
|
|
|
122
125
|
const FAST = 2500
|
|
123
126
|
const SLOW = 6000
|
|
124
127
|
const IDLE_AFTER = 60000
|
|
125
|
-
|
|
128
|
+
// Recycle the warm session after ONE cycle. Autonomy cycles are independent (a
|
|
129
|
+
// mention → a reply, a task → its work), so keeping a session across cycles just
|
|
130
|
+
// re-bills the entire prior history on every new cycle — the #1 token sink. Fresh
|
|
131
|
+
// per cycle keeps each cycle's cost to its own work (the static charter/creds ride
|
|
132
|
+
// in the cached system prompt, so a fresh spawn is cheap).
|
|
133
|
+
const MAX_TURNS = 1
|
|
126
134
|
const SESSION_IDLE_MS = 1200000
|
|
127
135
|
// A single cycle must finish within this or it's abandoned — otherwise a hung
|
|
128
136
|
// cycle (e.g. an MCP tool stalling on a down bridge) would leave `busy` stuck
|
|
@@ -227,7 +235,7 @@ export async function runWatch({ flags }) {
|
|
|
227
235
|
// The openvisio-team MCP is declared in an `opencode.json` written into the run cwd
|
|
228
236
|
// (opencode reads it from there). `--auto` approves tool use non-interactively.
|
|
229
237
|
// Same { runCycle, canCode } contract as the Claude runner.
|
|
230
|
-
function createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, maxCycleMs, log, debug, model }) {
|
|
238
|
+
function createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, maxCycleMs, log, debug, model, systemPrompt }) {
|
|
231
239
|
// opencode reads opencode.json from its CWD: the code workspace, or a dedicated
|
|
232
240
|
// per-agent dir for chat-only agents.
|
|
233
241
|
const cwd = workdir || join(OV_DIR, 'opencode-' + (cfgKey || 'agent'))
|
|
@@ -253,7 +261,10 @@ function createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, ma
|
|
|
253
261
|
return new Promise((resolve) => {
|
|
254
262
|
ensureConfig()
|
|
255
263
|
const m = cycleModel || model
|
|
256
|
-
|
|
264
|
+
// opencode has no system-prompt flag; each run is a fresh process, so fold the
|
|
265
|
+
// charter/creds into the message (still not re-accumulated across cycles).
|
|
266
|
+
const full = systemPrompt ? systemPrompt + '\n\n' + prompt : prompt
|
|
267
|
+
const args = ['run', full, '--auto', ...(m ? ['--model', m] : [])]
|
|
257
268
|
let child = null, done = false
|
|
258
269
|
const finish = (o) => { if (done) return; done = true; clearTimeout(timer); resolve(o) }
|
|
259
270
|
const timer = setTimeout(() => {
|
|
@@ -276,12 +287,12 @@ function createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, ma
|
|
|
276
287
|
// ── Claude Code warm-session cycle runner (shared by the REST + WS loops) ─────
|
|
277
288
|
// One persistent stream-json session, poked with a prompt per cycle. Recycled
|
|
278
289
|
// after MAX_TURNS or SESSION_IDLE_MS. Returns { runCycle, canCode }.
|
|
279
|
-
function createCycleRunner({ claude, agent, mcpUrl, mcpHeaders, cfgKey, mcpConfig, workdir, log, debug, model, onTool }) {
|
|
290
|
+
function createCycleRunner({ claude, agent, mcpUrl, mcpHeaders, cfgKey, mcpConfig, workdir, log, debug, model, onTool, systemPrompt }) {
|
|
280
291
|
const canCode = !!workdir
|
|
281
292
|
const maxCycleMs = canCode ? MAX_CODE_CYCLE_MS : MAX_CYCLE_MS
|
|
282
293
|
// opencode drives cycles differently — a headless `opencode run` per cycle rather
|
|
283
294
|
// than a persistent stream-json session. Same { runCycle, canCode } contract.
|
|
284
|
-
if (agent === 'opencode') return createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, maxCycleMs, log, debug, model })
|
|
295
|
+
if (agent === 'opencode') return createOpencodeRunner({ mcpUrl, mcpHeaders, cfgKey, workdir, canCode, maxCycleMs, log, debug, model, systemPrompt })
|
|
285
296
|
let child = null
|
|
286
297
|
// The model the CURRENT session was spawned with. runCycle can pass a different
|
|
287
298
|
// model per cycle (cheap for chat, stronger for code) — a change recycles the
|
|
@@ -312,7 +323,9 @@ function createCycleRunner({ claude, agent, mcpUrl, mcpHeaders, cfgKey, mcpConfi
|
|
|
312
323
|
function ensureSession() {
|
|
313
324
|
if (child && !child.killed) return
|
|
314
325
|
if (!mcpConfig) { log('WARNING: no MCP config — the agent can react to events but has no tools to act. Re-connect with --mcp-url.') }
|
|
315
|
-
|
|
326
|
+
// The charter + creds ride in the system prompt (cacheable → not re-billed each
|
|
327
|
+
// cycle), leaving only the small per-cycle instruction in the user message.
|
|
328
|
+
const base = ['-p', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose', '--strict-mcp-config', '--mcp-config', mcpConfig, ...(sessionModel ? ['--model', sessionModel] : []), ...(systemPrompt ? ['--append-system-prompt', systemPrompt] : [])]
|
|
316
329
|
const args = canCode ? [...base, '--allowedTools', ...CODE_TOOLS, '--disallowedTools', ...DENY_TOOLS] : [...base, '--allowedTools', 'mcp__openvisio-team__*']
|
|
317
330
|
const c = spawn(claude, args, { cwd: workdir || undefined, stdio: ['pipe', 'pipe', 'inherit'] })
|
|
318
331
|
child = c
|
|
@@ -387,9 +400,16 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
387
400
|
// agent:status broadcast (thinking → working → typing → done).
|
|
388
401
|
const statusTargets = new Set()
|
|
389
402
|
const emitStatus = (state) => { for (const c of statusTargets) { try { handle && handle.sendStatus(c, state) } catch { /* best-effort */ } } }
|
|
390
|
-
const
|
|
403
|
+
const canCode = !!workdir
|
|
404
|
+
// The Mastra bridge authenticates per-CALL: every openvisio-team tool needs
|
|
405
|
+
// agent_identifier + agent_api_key as arguments. Hand them over up front.
|
|
406
|
+
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.)`
|
|
407
|
+
// The STATIC charter + creds are the session system prompt (cached, billed once),
|
|
408
|
+
// NOT re-sent in every cycle's user message — the big token saving.
|
|
409
|
+
const systemPrompt = (canCode ? CODE_CHARTER : CHAT_CHARTER) + '\n\n' + credNote
|
|
410
|
+
const { runCycle } = createCycleRunner({
|
|
391
411
|
claude, agent, mcpUrl, mcpHeaders: { 'x-agent-api-key': apiKey, 'x-agent-identifier': identifier },
|
|
392
|
-
cfgKey: identifier, mcpConfig, workdir, log, debug, model,
|
|
412
|
+
cfgKey: identifier, mcpConfig, workdir, log, debug, model, systemPrompt,
|
|
393
413
|
// The moment the agent calls post_message it is about to speak → "typing".
|
|
394
414
|
onTool: (name) => { if (/post_message/.test(name)) emitStatus('typing') },
|
|
395
415
|
})
|
|
@@ -417,11 +437,6 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
417
437
|
// events and drained into the next cycle's prompt.
|
|
418
438
|
const pending = []
|
|
419
439
|
|
|
420
|
-
// The Mastra bridge authenticates per-CALL, not per-connection: every
|
|
421
|
-
// openvisio-team tool needs agent_identifier + agent_api_key as arguments.
|
|
422
|
-
// Hand them to the model up front so it never shells around hunting for them.
|
|
423
|
-
const credNote = `AUTH: the openvisio-team (mcp__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, …). They are given to you right here — do NOT hunt for them (no Bash/grep/cat/find to locate credentials, 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.)`
|
|
424
|
-
|
|
425
440
|
// Higher rank wins when coalescing cycles requested while one is running.
|
|
426
441
|
const RANK = { fast: 0, intro: 1, sweep: 2, full: 3 }
|
|
427
442
|
const baseFor = (kind) => kind === 'intro' ? INTRO : (kind === 'full' || kind === 'sweep') ? fullPrompt : fastPrompt
|
|
@@ -431,7 +446,9 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
431
446
|
if (busy) { queued = (RANK[kind] ?? 0) >= (RANK[queued] ?? 0) ? kind : queued; log('busy — queued a ' + kind + ' follow-up cycle'); return }
|
|
432
447
|
busy = true
|
|
433
448
|
const ctx = pending.splice(0) // take everything accumulated so far
|
|
434
|
-
|
|
449
|
+
// credNote + charter live in the cached system prompt now — the per-cycle
|
|
450
|
+
// message is just the event context + the small base instruction.
|
|
451
|
+
const prompt = (ctx.length ? ctx.join('\n') + '\n\n' : '') + baseFor(kind)
|
|
435
452
|
// Chat-shaped cycles (mentions/intro) may run on the cheaper chat model; code
|
|
436
453
|
// work (full/sweep) uses the main model.
|
|
437
454
|
const useModel = (kind === 'fast' || kind === 'intro') ? liteModel : codeModel
|
|
@@ -552,7 +569,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
552
569
|
// takes it to working → typing → done).
|
|
553
570
|
if (cid != null) { statusTargets.add(cid); try { handle && handle.sendStatus(cid, 'thinking') } catch { /* best-effort */ } }
|
|
554
571
|
const ctx = cid != null
|
|
555
|
-
? `You were @mentioned in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". This mention is FOR YOU. Send EXACTLY ONE reply with
|
|
572
|
+
? `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.`
|
|
556
573
|
: undefined
|
|
557
574
|
void drain('fast', ctx)
|
|
558
575
|
} else if (k === 'error') {
|
|
@@ -609,7 +626,11 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
609
626
|
function loop({ host, key, slug, claude, agent, mcpConfig, mcpUrl, workdir, model, chatModel, debug }) {
|
|
610
627
|
const log = (m) => process.stdout.write('[warm ' + new Date().toISOString() + '] ' + m + '\n')
|
|
611
628
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
612
|
-
const
|
|
629
|
+
const canCode = !!workdir
|
|
630
|
+
// Static charter as the cached system prompt (this loop's MCP authenticates via a
|
|
631
|
+
// Bearer header, so no per-call creds are needed in the prompt).
|
|
632
|
+
const systemPrompt = canCode ? CODE_CHARTER : CHAT_CHARTER
|
|
633
|
+
const { runCycle } = createCycleRunner({ claude, agent, mcpUrl, mcpHeaders: { Authorization: 'Bearer ' + key }, cfgKey: slug, mcpConfig, workdir, log, debug, model, systemPrompt })
|
|
613
634
|
const fullPrompt = canCode ? CODE_FULL : CYCLE
|
|
614
635
|
const fastPrompt = canCode ? CODE_FAST : CYCLE_FAST
|
|
615
636
|
const liteModel = chatModel || model // cheaper model for chat/quick cycles
|