openvisio-agent 0.8.1 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/watch.mjs +40 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openvisio-agent",
3
- "version": "0.8.1",
3
+ "version": "0.9.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
@@ -40,8 +40,11 @@ const CHAT_CHARTER = [
40
40
  REPLY_DISCIPLINE,
41
41
  ].join('\n')
42
42
 
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
- const CYCLE_FAST = CHAT_CHARTER + '\n\n' + [
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
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.',
@@ -67,7 +70,7 @@ const CODE_CHARTER = [
67
70
  REPLY_DISCIPLINE,
68
71
  ].join('\n')
69
72
 
70
- const CODE_FULL = CODE_CHARTER + '\n\n' + [
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,7 +83,7 @@ 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 = CODE_CHARTER + '\n\n' + [
86
+ const CODE_FAST = [
84
87
  'New chat activity. Do EXACTLY ONE of these:',
85
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.',
@@ -122,7 +125,12 @@ const DENY_TOOLS = [
122
125
  const FAST = 2500
123
126
  const SLOW = 6000
124
127
  const IDLE_AFTER = 60000
125
- const MAX_TURNS = 15
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
- const args = ['run', prompt, '--auto', ...(m ? ['--model', m] : [])]
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
- const base = ['-p', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose', '--strict-mcp-config', '--mcp-config', mcpConfig, ...(sessionModel ? ['--model', sessionModel] : [])]
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 { runCycle, canCode } = createCycleRunner({
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 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.)`
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
- const prompt = credNote + '\n\n' + (ctx.length ? ctx.join('\n') + '\n\n' : '') + baseFor(kind)
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
@@ -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 { runCycle, canCode } = createCycleRunner({ claude, agent, mcpUrl, mcpHeaders: { Authorization: 'Bearer ' + key }, cfgKey: slug, mcpConfig, workdir, log, debug, model })
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