opencode-raven 2.1.7 → 2.2.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/CHANGELOG.md +25 -0
- package/README.md +66 -42
- package/Raven.md +17 -35
- package/RavenMcp.md +42 -0
- package/index.ts +701 -268
- package/mcp-guidance.md +33 -31
- package/package.json +4 -2
package/index.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { homedir, tmpdir } from "node:os"
|
|
|
11
11
|
// ── Resolve paths relative to this package (works in node_modules/) ──
|
|
12
12
|
const PKG_DIR = import.meta.dirname!
|
|
13
13
|
|
|
14
|
-
const RAVEN_MD = join(PKG_DIR, "Raven.md")
|
|
14
|
+
const RAVEN_MD = join(PKG_DIR, "Raven.md")
|
|
15
|
+
const RAVEN_MCP_MD = join(PKG_DIR, "RavenMcp.md")
|
|
15
16
|
const MCP_GUIDANCE_MD = join(PKG_DIR, "mcp-guidance.md")
|
|
16
17
|
const PACKAGE_JSON = JSON.parse(readFileSync(join(PKG_DIR, "package.json"), "utf-8"))
|
|
17
18
|
const PACKAGE_NAME = PACKAGE_JSON.name || "opencode-raven"
|
|
@@ -62,7 +63,7 @@ const DEFAULT_ON_DEMAND_MCP_METADATA: Record<string, OnDemandMcpMetadata> = {
|
|
|
62
63
|
},
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
const NEVER_ROUTE_TOOLS = new Set(["raven_seek", "raven_mcp", "task", "subtask"])
|
|
66
|
+
const NEVER_ROUTE_TOOLS = new Set(["raven_seek", "raven_mcp", "raven_mcp_bridge", "task", "subtask"])
|
|
66
67
|
const MAX_TIMER_MS = 2_147_483_647
|
|
67
68
|
const MAX_TIMEOUT_SECONDS = Math.floor(MAX_TIMER_MS / 1000)
|
|
68
69
|
|
|
@@ -214,38 +215,58 @@ type OnDemandMcpServer = RemoteOnDemandMcpServer | LocalOnDemandMcpServer
|
|
|
214
215
|
type OnDemandMcpDescriptionDetail = "full" | "minimized"
|
|
215
216
|
type RavenMcpOperation = "list_tools" | "call_tool" | "list_resources" | "read_resource" | "list_prompts" | "get_prompt"
|
|
216
217
|
|
|
217
|
-
interface
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
218
|
+
interface RavenSeekConfig {
|
|
219
|
+
model?: string
|
|
220
|
+
reasoning_effort?: string
|
|
221
|
+
instructions?: string
|
|
222
|
+
routeTools?: string[]
|
|
223
|
+
routeToolKeywords?: string[]
|
|
224
|
+
excludeAgents?: string[]
|
|
225
|
+
excludeTools?: string[]
|
|
226
|
+
timeout?: number
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
interface RavenMcpConfig {
|
|
230
|
+
model?: string
|
|
231
|
+
reasoning_effort?: string
|
|
232
|
+
instructions?: string
|
|
233
|
+
timeout?: number
|
|
234
|
+
descriptionDetail?: OnDemandMcpDescriptionDetail
|
|
235
|
+
onDemandMcpServers?: Record<string, OnDemandMcpServer>
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
interface RavenConfig {
|
|
239
|
+
raven_seek: RavenSeekConfig
|
|
240
|
+
raven_mcp: RavenMcpConfig
|
|
241
|
+
stats?: { bytes: number }
|
|
242
|
+
}
|
|
231
243
|
|
|
232
244
|
// ── Parse Raven.md frontmatter ──
|
|
233
|
-
const ravenMd = readFileSync(RAVEN_MD, "utf-8")
|
|
234
|
-
const { frontmatter: fm, prompt: ravenPrompt } = parseRavenMd(ravenMd)
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
245
|
+
const ravenMd = readFileSync(RAVEN_MD, "utf-8")
|
|
246
|
+
const { frontmatter: fm, prompt: ravenPrompt } = parseRavenMd(ravenMd)
|
|
247
|
+
const ravenMcpMd = readFileSync(RAVEN_MCP_MD, "utf-8")
|
|
248
|
+
const { frontmatter: mcpFm, prompt: ravenMcpPrompt } = parseRavenMd(ravenMcpMd)
|
|
249
|
+
|
|
250
|
+
const DEFAULT_CONFIG: RavenConfig = {
|
|
251
|
+
raven_seek: {
|
|
252
|
+
model: fm.model,
|
|
253
|
+
reasoning_effort: fm.reasoning_effort,
|
|
254
|
+
instructions: "",
|
|
255
|
+
routeTools: DEFAULT_ROUTE_TOOLS,
|
|
256
|
+
routeToolKeywords: DEFAULT_ROUTE_TOOL_KEYWORDS,
|
|
257
|
+
excludeAgents: [],
|
|
258
|
+
excludeTools: [],
|
|
259
|
+
timeout: 600,
|
|
260
|
+
},
|
|
261
|
+
raven_mcp: {
|
|
262
|
+
model: mcpFm.model,
|
|
263
|
+
reasoning_effort: mcpFm.reasoning_effort,
|
|
264
|
+
instructions: "",
|
|
265
|
+
timeout: 600,
|
|
266
|
+
descriptionDetail: "full",
|
|
267
|
+
onDemandMcpServers: DEFAULT_ON_DEMAND_MCP_SERVERS,
|
|
268
|
+
},
|
|
269
|
+
}
|
|
249
270
|
|
|
250
271
|
function parseRavenMd(raw: string): { frontmatter: Record<string, any>; prompt: string } {
|
|
251
272
|
const parts = raw.split("---")
|
|
@@ -440,26 +461,43 @@ export default (async (input: PluginInput) => {
|
|
|
440
461
|
const mcpMetadataFile = join(ravenConfigDir, "autogenerated-on-demand-mcp-metadata.json")
|
|
441
462
|
const dynamicGuidanceFile = join(ravenConfigDir, "autogenerated-on-demand-guidance.md")
|
|
442
463
|
|
|
443
|
-
function normalizeConfig(raw: any): RavenConfig {
|
|
444
|
-
const source = raw && typeof raw === "object" ? raw : {}
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
normalized
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
:
|
|
464
|
+
function normalizeConfig(raw: any): RavenConfig {
|
|
465
|
+
const source = raw && typeof raw === "object" ? raw : {}
|
|
466
|
+
const seekSource = source.raven_seek && typeof source.raven_seek === "object" ? source.raven_seek : source
|
|
467
|
+
const mcpSource = source.raven_mcp && typeof source.raven_mcp === "object" ? source.raven_mcp : source
|
|
468
|
+
const legacyInstructions = typeof source.ravenInstructions === "string" ? source.ravenInstructions : undefined
|
|
469
|
+
const normalized: RavenConfig = {
|
|
470
|
+
raven_seek: {
|
|
471
|
+
model: typeof seekSource.model === "string" ? seekSource.model : DEFAULT_CONFIG.raven_seek.model,
|
|
472
|
+
reasoning_effort: typeof seekSource.reasoning_effort === "string" ? seekSource.reasoning_effort : DEFAULT_CONFIG.raven_seek.reasoning_effort,
|
|
473
|
+
instructions: typeof seekSource.instructions === "string" ? seekSource.instructions : legacyInstructions ?? "",
|
|
474
|
+
routeTools: uniqueStrings(seekSource.routeTools, DEFAULT_ROUTE_TOOLS),
|
|
475
|
+
routeToolKeywords: uniqueStrings(seekSource.routeToolKeywords, DEFAULT_ROUTE_TOOL_KEYWORDS),
|
|
476
|
+
excludeAgents: uniqueStrings(seekSource.excludeAgents),
|
|
477
|
+
excludeTools: uniqueStrings(seekSource.excludeTools),
|
|
478
|
+
timeout: typeof seekSource.timeout === "number" && Number.isFinite(seekSource.timeout)
|
|
479
|
+
&& seekSource.timeout >= 10 && seekSource.timeout <= MAX_TIMEOUT_SECONDS
|
|
480
|
+
? Math.floor(seekSource.timeout)
|
|
481
|
+
: DEFAULT_CONFIG.raven_seek.timeout,
|
|
482
|
+
},
|
|
483
|
+
raven_mcp: {
|
|
484
|
+
model: typeof mcpSource.model === "string" ? mcpSource.model : DEFAULT_CONFIG.raven_mcp.model,
|
|
485
|
+
reasoning_effort: typeof mcpSource.reasoning_effort === "string" ? mcpSource.reasoning_effort : DEFAULT_CONFIG.raven_mcp.reasoning_effort,
|
|
486
|
+
instructions: typeof mcpSource.instructions === "string" ? mcpSource.instructions : legacyInstructions ?? "",
|
|
487
|
+
timeout: typeof mcpSource.timeout === "number" && Number.isFinite(mcpSource.timeout)
|
|
488
|
+
&& mcpSource.timeout >= 10 && mcpSource.timeout <= MAX_TIMEOUT_SECONDS
|
|
489
|
+
? Math.floor(mcpSource.timeout)
|
|
490
|
+
: DEFAULT_CONFIG.raven_mcp.timeout,
|
|
491
|
+
descriptionDetail: mcpSource.descriptionDetail === "minimized"
|
|
492
|
+
? "minimized"
|
|
493
|
+
: mcpSource.descriptionDetail === "full"
|
|
494
|
+
? "full"
|
|
495
|
+
: source.onDemandMcpDescriptionDetail === "minimized" ? "minimized" : "full",
|
|
496
|
+
onDemandMcpServers: mcpSource.onDemandMcpServers === undefined
|
|
497
|
+
? cloneOnDemandMcpServers(DEFAULT_ON_DEMAND_MCP_SERVERS)
|
|
498
|
+
: normalizeOnDemandMcpServers(mcpSource.onDemandMcpServers),
|
|
499
|
+
},
|
|
500
|
+
}
|
|
463
501
|
normalized.stats = source.stats && typeof source.stats === "object"
|
|
464
502
|
&& typeof source.stats.bytes === "number" && Number.isFinite(source.stats.bytes) && source.stats.bytes >= 0
|
|
465
503
|
? { bytes: Math.round(source.stats.bytes) }
|
|
@@ -472,7 +510,9 @@ export default (async (input: PluginInput) => {
|
|
|
472
510
|
if (existsSync(configFile)) {
|
|
473
511
|
try {
|
|
474
512
|
const raw = JSON.parse(readFileSync(configFile, "utf-8"))
|
|
475
|
-
|
|
513
|
+
const normalized = normalizeConfig(raw)
|
|
514
|
+
if (!raw.raven_seek || !raw.raven_mcp) writeConfig(normalized)
|
|
515
|
+
return normalized
|
|
476
516
|
} catch {
|
|
477
517
|
// Preserve malformed or partially-written config so the user can repair it.
|
|
478
518
|
return { ...DEFAULT_CONFIG }
|
|
@@ -487,7 +527,7 @@ export default (async (input: PluginInput) => {
|
|
|
487
527
|
try {
|
|
488
528
|
mkdirSync(ravenConfigDir, { recursive: true })
|
|
489
529
|
writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n")
|
|
490
|
-
} catch { /* non-fatal: config
|
|
530
|
+
} catch { /* non-fatal: config changes remain in-session */ }
|
|
491
531
|
}
|
|
492
532
|
|
|
493
533
|
function loadMcpMetadata(): Record<string, OnDemandMcpMetadata> {
|
|
@@ -522,18 +562,31 @@ export default (async (input: PluginInput) => {
|
|
|
522
562
|
const raw = existsSync(configFile) ? JSON.parse(readFileSync(configFile, "utf-8")) : config
|
|
523
563
|
const latest = normalizeConfig(raw)
|
|
524
564
|
const patch = typeof update === "function" ? update(latest) : update
|
|
525
|
-
config = normalizeConfig({
|
|
526
|
-
|
|
565
|
+
config = normalizeConfig({
|
|
566
|
+
...latest,
|
|
567
|
+
...patch,
|
|
568
|
+
raven_seek: { ...latest.raven_seek, ...patch.raven_seek },
|
|
569
|
+
raven_mcp: { ...latest.raven_mcp, ...patch.raven_mcp },
|
|
570
|
+
})
|
|
571
|
+
writeConfig(config)
|
|
527
572
|
} catch {
|
|
528
573
|
// Keep malformed or partially-written external edits intact instead of replacing them.
|
|
529
574
|
const patch = typeof update === "function" ? update(config) : update
|
|
530
|
-
config = normalizeConfig({
|
|
575
|
+
config = normalizeConfig({
|
|
576
|
+
...config,
|
|
577
|
+
...patch,
|
|
578
|
+
raven_seek: { ...config.raven_seek, ...patch.raven_seek },
|
|
579
|
+
raven_mcp: { ...config.raven_mcp, ...patch.raven_mcp },
|
|
580
|
+
})
|
|
531
581
|
}
|
|
532
582
|
return config
|
|
533
583
|
}
|
|
534
584
|
let mcpMetadata = loadMcpMetadata()
|
|
535
|
-
|
|
536
|
-
const
|
|
585
|
+
type DelegationKind = "seek" | "mcp"
|
|
586
|
+
const ravenSessions = new Set<string>()
|
|
587
|
+
const ravenSessionKinds = new Map<string, DelegationKind>()
|
|
588
|
+
const activeRavenPrompts = new Set<string>()
|
|
589
|
+
const ravenTaskCalls = new Set<string>()
|
|
537
590
|
const sessionAgents = new Map<string, string>()
|
|
538
591
|
const ravenSessionParents = new Map<string, string>()
|
|
539
592
|
const ravenErrorWaiters = new Map<string, (error: Error) => void>()
|
|
@@ -549,10 +602,12 @@ export default (async (input: PluginInput) => {
|
|
|
549
602
|
let mcpRefreshPromise: Promise<{ refreshed: string[]; failed: string[] }> | undefined
|
|
550
603
|
let mcpRefreshQueue: Promise<unknown> = Promise.resolve()
|
|
551
604
|
const mcpRefreshControllers = new Set<AbortController>()
|
|
552
|
-
let lastNotifiedMcpFailureKey = ""
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
605
|
+
let lastNotifiedMcpFailureKey = ""
|
|
606
|
+
const ravenProviderIssues: Partial<Record<DelegationKind, string>> = {}
|
|
607
|
+
|
|
608
|
+
// Replace generated instructions from older releases before OpenCode consumes them.
|
|
609
|
+
saveDynamicGuidance()
|
|
610
|
+
resetOnDemandMcpRuntimeStatuses()
|
|
556
611
|
|
|
557
612
|
function resetOnDemandMcpRuntimeStatuses() {
|
|
558
613
|
let changed = false
|
|
@@ -569,17 +624,17 @@ export default (async (input: PluginInput) => {
|
|
|
569
624
|
}
|
|
570
625
|
|
|
571
626
|
// ── Check if an agent is excluded from Raven enforcement (case-insensitive) ──
|
|
572
|
-
function isExcluded(agent: string | undefined): boolean {
|
|
573
|
-
if (!agent || !config.excludeAgents?.length) return false
|
|
574
|
-
const lower = agent.toLowerCase()
|
|
575
|
-
return config.excludeAgents.some((a) => a.toLowerCase() === lower)
|
|
576
|
-
}
|
|
627
|
+
function isExcluded(agent: string | undefined): boolean {
|
|
628
|
+
if (!agent || !config.raven_seek.excludeAgents?.length) return false
|
|
629
|
+
const lower = agent.toLowerCase()
|
|
630
|
+
return config.raven_seek.excludeAgents.some((a) => a.toLowerCase() === lower)
|
|
631
|
+
}
|
|
577
632
|
|
|
578
633
|
function isRouteConfigured(toolName: string): boolean {
|
|
579
634
|
const tool = toolName.toLowerCase()
|
|
580
635
|
if (NEVER_ROUTE_TOOLS.has(tool)) return false
|
|
581
|
-
if (config.routeTools?.some((name) => name.toLowerCase() === tool)) return true
|
|
582
|
-
if (config.routeToolKeywords?.some((keyword) => tool.includes(keyword.toLowerCase()))) return true
|
|
636
|
+
if (config.raven_seek.routeTools?.some((name) => name.toLowerCase() === tool)) return true
|
|
637
|
+
if (config.raven_seek.routeToolKeywords?.some((keyword) => tool.includes(keyword.toLowerCase()))) return true
|
|
583
638
|
return globalMcpServers.some((server) => tool.startsWith(`${server.toLowerCase()}_`))
|
|
584
639
|
}
|
|
585
640
|
|
|
@@ -609,31 +664,32 @@ export default (async (input: PluginInput) => {
|
|
|
609
664
|
return query.length > 300 ? `${query.slice(0, 297)}...` : query
|
|
610
665
|
}
|
|
611
666
|
|
|
612
|
-
function rerouteMessage(tool: string, args: any): string {
|
|
613
|
-
if (
|
|
614
|
-
return `Raven is currently unavailable: ${
|
|
615
|
-
|
|
616
|
-
${ravenRecoveryInstructions()}`
|
|
617
|
-
}
|
|
667
|
+
function rerouteMessage(tool: string, args: any): string {
|
|
668
|
+
if (ravenProviderIssues.seek) {
|
|
669
|
+
return `Raven search is currently unavailable: ${ravenProviderIssues.seek}
|
|
670
|
+
|
|
671
|
+
${ravenRecoveryInstructions("seek")}`
|
|
672
|
+
}
|
|
618
673
|
return `The '${tool}' tool call is blocked by Raven. Your next tool call should be raven_seek(query="${attemptedQuery(tool, args).replace(/"/g, "'")}").`
|
|
619
674
|
}
|
|
620
675
|
|
|
621
|
-
function ravenRecoveryInstructions(): string {
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
676
|
+
function ravenRecoveryInstructions(kind: DelegationKind): string {
|
|
677
|
+
const command = kind === "mcp" ? "/raven mcp model <provider/model>" : "/raven seek model <provider/model>"
|
|
678
|
+
return `Stop trying Raven-routed tools or MCPs for this request. Inform the user that Raven cannot run until its model/provider is changed or fixed.
|
|
679
|
+
|
|
680
|
+
Tell the user to switch Raven's model with:
|
|
681
|
+
${command}
|
|
682
|
+
|
|
683
|
+
Example:
|
|
684
|
+
${command.replace("<provider/model>", "openai/gpt-5.5")}
|
|
629
685
|
|
|
630
686
|
Then restart opencode for the model change to take effect. If the task can be completed without Raven, continue without routed tools and clearly mention what could not be checked.`
|
|
631
687
|
}
|
|
632
688
|
|
|
633
|
-
function routeSummary(): string {
|
|
634
|
-
const tools = config.routeTools?.length ? config.routeTools.join(", ") : "(none)"
|
|
635
|
-
const mcps = globalMcpServers.length ? `${globalMcpServers.join(", ")} (auto-detected)` : "none detected"
|
|
636
|
-
const keywords = config.routeToolKeywords?.length ? config.routeToolKeywords.join(", ") : "(none)"
|
|
689
|
+
function routeSummary(): string {
|
|
690
|
+
const tools = config.raven_seek.routeTools?.length ? config.raven_seek.routeTools.join(", ") : "(none)"
|
|
691
|
+
const mcps = globalMcpServers.length ? `${globalMcpServers.join(", ")} (auto-detected)` : "none detected"
|
|
692
|
+
const keywords = config.raven_seek.routeToolKeywords?.length ? config.raven_seek.routeToolKeywords.join(", ") : "(none)"
|
|
637
693
|
const duplicates = duplicateMcpServers.length ? `\n Duplicate MCP config: ${duplicateMcpServers.join(", ")} (configured globally and on-demand)` : ""
|
|
638
694
|
return `Raven routing:\n Routed tools: ${tools}\n Routed global MCPs: ${mcps}\n Routed tool keywords: ${keywords}${duplicates}`
|
|
639
695
|
}
|
|
@@ -641,23 +697,29 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
641
697
|
function mcpSummary(): string {
|
|
642
698
|
const buckets = onDemandMcpStatusBuckets()
|
|
643
699
|
const total = buckets.loaded.length + buckets.failed.length + buckets.pending.length
|
|
644
|
-
if (!total) return `Raven on-demand MCPs (always behind Raven): none configured.\n Guidance detail: ${config.
|
|
700
|
+
if (!total) return `Raven on-demand MCPs (always behind Raven): none configured.\n Guidance detail: ${config.raven_mcp.descriptionDetail ?? "full"}`
|
|
645
701
|
const lines = [
|
|
646
702
|
"Raven on-demand MCPs (always behind Raven):",
|
|
647
703
|
` Loaded: ${buckets.loaded.length ? buckets.loaded.join(", ") : "none"}`,
|
|
648
704
|
` Failed: ${buckets.failed.length ? buckets.failed.join(", ") : "none"}`,
|
|
649
705
|
` Pending: ${buckets.pending.length ? buckets.pending.join(", ") : "none"}`,
|
|
650
|
-
` Guidance detail: ${config.
|
|
706
|
+
` Guidance detail: ${config.raven_mcp.descriptionDetail ?? "full"}`,
|
|
651
707
|
]
|
|
652
708
|
return lines.join("\n")
|
|
653
709
|
}
|
|
654
710
|
|
|
655
|
-
function ravenAgentPrompt(): string {
|
|
656
|
-
const extra = config.
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
711
|
+
function ravenAgentPrompt(): string {
|
|
712
|
+
const extra = config.raven_seek.instructions?.trim()
|
|
713
|
+
const prompt = ravenPrompt
|
|
714
|
+
return extra ? `${prompt}\n\nAdditional user instructions:\n${extra}` : prompt
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function ravenMcpAgentPrompt(): string {
|
|
718
|
+
const extra = config.raven_mcp.instructions?.trim()
|
|
719
|
+
const onDemand = onDemandMcpGuidance("raven")
|
|
720
|
+
const prompt = `${ravenMcpPrompt}\n\nOn-demand MCP guidance:\n${onDemand}`
|
|
721
|
+
return extra ? `${prompt}\n\nAdditional user instructions:\n${extra}` : prompt
|
|
722
|
+
}
|
|
661
723
|
|
|
662
724
|
function compactText(value: unknown): string {
|
|
663
725
|
return String(value ?? "").replace(/\s+/g, " ").trim()
|
|
@@ -699,7 +761,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
699
761
|
}
|
|
700
762
|
|
|
701
763
|
function onDemandMcpGuidance(target: "delegate" | "raven" = "delegate", preferredOrder?: string[]): string {
|
|
702
|
-
const servers = config.onDemandMcpServers ?? {}
|
|
764
|
+
const servers = config.raven_mcp.onDemandMcpServers ?? {}
|
|
703
765
|
const activeEntries = activeOnDemandMcpEntries()
|
|
704
766
|
const entries = orderedOnDemandMcpEntries(
|
|
705
767
|
target === "raven"
|
|
@@ -711,7 +773,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
711
773
|
if (!activeEntries.length) return "No enabled on-demand MCPs are configured."
|
|
712
774
|
if (!entries.length) return "No loaded on-demand MCPs are available."
|
|
713
775
|
|
|
714
|
-
const detail = config.
|
|
776
|
+
const detail = config.raven_mcp.descriptionDetail ?? "full"
|
|
715
777
|
const lines = entries.map(([name, server]) => {
|
|
716
778
|
const metadata = mcpMetadata[name] ?? {}
|
|
717
779
|
const userDescription = firstSentenceText(server.description)
|
|
@@ -724,10 +786,10 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
724
786
|
.join("; ")
|
|
725
787
|
return `- ${name}: ${summary} Tools: ${tools}`
|
|
726
788
|
})
|
|
727
|
-
if (target === "raven") {
|
|
728
|
-
return `
|
|
789
|
+
if (target === "raven") {
|
|
790
|
+
return `Use raven_mcp_bridge for these on-demand MCP capabilities:\n${lines.join("\n")}`
|
|
729
791
|
}
|
|
730
|
-
return `Raven can
|
|
792
|
+
return `Non-Raven agents can use these on-demand MCP capabilities through the public raven_mcp delegator. Use raven_mcp for matching requests and include the target MCP server and likely capability when obvious. The raven-mcp agent itself must call raven_mcp_bridge directly instead:\n${lines.join("\n")}`
|
|
731
793
|
}
|
|
732
794
|
|
|
733
795
|
function resolveEnvPlaceholders(value: string): string {
|
|
@@ -816,7 +878,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
816
878
|
let mcpIdleTimer: ReturnType<typeof setInterval> | undefined
|
|
817
879
|
|
|
818
880
|
function mcpTimeoutMs(server: OnDemandMcpServer): number {
|
|
819
|
-
return server.timeout ?? (config.timeout ?? 600) * 1000
|
|
881
|
+
return server.timeout ?? (config.raven_mcp.timeout ?? 600) * 1000
|
|
820
882
|
}
|
|
821
883
|
|
|
822
884
|
async function closeMcpClient(client: Client, transport: any) {
|
|
@@ -917,7 +979,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
917
979
|
}
|
|
918
980
|
|
|
919
981
|
function activeOnDemandMcpEntries(): [string, OnDemandMcpServer][] {
|
|
920
|
-
return Object.entries(config.onDemandMcpServers ?? {}).filter(([, server]) => server.enabled !== false)
|
|
982
|
+
return Object.entries(config.raven_mcp.onDemandMcpServers ?? {}).filter(([, server]) => server.enabled !== false)
|
|
921
983
|
}
|
|
922
984
|
|
|
923
985
|
function waitForMcpConnection(attempt: McpConnectionAttempt, signal?: AbortSignal): Promise<McpConnection> {
|
|
@@ -1214,7 +1276,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1214
1276
|
|
|
1215
1277
|
function onDemandMcpStatus(): string {
|
|
1216
1278
|
const entries = activeOnDemandMcpEntries()
|
|
1217
|
-
if (!Object.keys(config.onDemandMcpServers ?? {}).length) return "Raven on-demand MCPs: none configured."
|
|
1279
|
+
if (!Object.keys(config.raven_mcp.onDemandMcpServers ?? {}).length) return "Raven on-demand MCPs: none configured."
|
|
1218
1280
|
if (!entries.length) return "Raven on-demand MCPs: none enabled."
|
|
1219
1281
|
const buckets = onDemandMcpStatusBuckets()
|
|
1220
1282
|
const lines = entries.map(([name, server]) => {
|
|
@@ -1227,7 +1289,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1227
1289
|
const error = status === "failed" && metadata?.lastError ? `, error: ${metadata.lastError}` : ""
|
|
1228
1290
|
return `- ${name} (${server.type}): ${displayMcpStatus(status)}, ${source}, ${generated}, ${tools}${avoided}${error}`
|
|
1229
1291
|
})
|
|
1230
|
-
return `Raven on-demand MCPs (${config.
|
|
1292
|
+
return `Raven on-demand MCPs (${config.raven_mcp.descriptionDetail ?? "full"} guidance):\n Loaded: ${buckets.loaded.length ? buckets.loaded.join(", ") : "none"}\n Failed: ${buckets.failed.length ? buckets.failed.join(", ") : "none"}\n Pending: ${buckets.pending.length ? buckets.pending.join(", ") : "none"}\n${lines.join("\n")}`
|
|
1231
1293
|
}
|
|
1232
1294
|
|
|
1233
1295
|
function avoidedMcpSchemaBytes(): number {
|
|
@@ -1243,9 +1305,9 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1243
1305
|
return `Raven context saved:\n Avoided MCP schema load: ${formatBytes(avoided)} (~${formatTokens(avoided)} context)\n This session: ${formatBytes(sessionBytes)} (~${formatTokens(sessionBytes)} context)\n All time: ${formatBytes(totalBytes)} (~${formatTokens(totalBytes)} context)`
|
|
1244
1306
|
}
|
|
1245
1307
|
|
|
1246
|
-
function helpText(): string {
|
|
1247
|
-
return `Raven commands:\n /raven help
|
|
1248
|
-
}
|
|
1308
|
+
function helpText(): string {
|
|
1309
|
+
return `Raven commands:\n /raven help - show this help\n /raven route - show or edit routed tools/keywords\n /raven seek model <name> - change the search model (requires restart)\n /raven seek effort <value> - change search reasoning effort (requires restart)\n /raven seek timeout <seconds> - change search timeout\n /raven mcp - show on-demand MCP metadata\n /raven mcp refresh [server] - refresh MCP metadata\n /raven mcp detail full|minimized - change MCP guidance detail\n /raven mcp model <name> - change the MCP model (requires restart)\n /raven mcp effort <value> - change MCP reasoning effort (requires restart)\n /raven mcp timeout <seconds> - change MCP delegation timeout\n /raven update - check for an update\n /raven stats - show context saved`
|
|
1310
|
+
}
|
|
1249
1311
|
|
|
1250
1312
|
function formatMcpList(title: string, items: any[], fields: string[] = ["name", "description"]): string {
|
|
1251
1313
|
if (!items.length) return `${title}: none found.`
|
|
@@ -1346,12 +1408,10 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1346
1408
|
return { current: PACKAGE_VERSION, latest, available: !!latest && compareVersions(latest, PACKAGE_VERSION) > 0 }
|
|
1347
1409
|
}
|
|
1348
1410
|
|
|
1349
|
-
|
|
1350
|
-
const
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
m?.info?.role === "assistant" && m?.info?.tokens?.output > 0
|
|
1354
|
-
)
|
|
1411
|
+
function estimateRavenSavedCandidateBytes(messages: any[]): number {
|
|
1412
|
+
const assistantMessages = messages.filter((m: any) =>
|
|
1413
|
+
m?.info?.role === "assistant" && m?.info?.tokens?.output > 0
|
|
1414
|
+
)
|
|
1355
1415
|
const first = assistantMessages[0]
|
|
1356
1416
|
const last = assistantMessages[assistantMessages.length - 1]
|
|
1357
1417
|
const t = last?.info?.tokens
|
|
@@ -1363,9 +1423,15 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1363
1423
|
const baselineTokens = firstTokens
|
|
1364
1424
|
? (firstTokens.input ?? 0) + (firstTokens.cache?.read ?? 0) + (firstTokens.cache?.write ?? 0)
|
|
1365
1425
|
: 0
|
|
1366
|
-
const savedCandidateTokens = Math.max(0, totalTokens - baselineTokens)
|
|
1367
|
-
return savedCandidateTokens * 4
|
|
1368
|
-
}
|
|
1426
|
+
const savedCandidateTokens = Math.max(0, totalTokens - baselineTokens)
|
|
1427
|
+
return savedCandidateTokens * 4
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
async function countRavenSavedCandidateBytes(sessionId: string, signal?: AbortSignal): Promise<number> {
|
|
1431
|
+
const messagesResp = await client.session.messages({ path: { id: sessionId }, signal })
|
|
1432
|
+
const messages = (messagesResp as any)?.data ?? []
|
|
1433
|
+
return estimateRavenSavedCandidateBytes(messages)
|
|
1434
|
+
}
|
|
1369
1435
|
|
|
1370
1436
|
async function getUpdateInfo(): Promise<{ current: string; latest?: string; available: boolean }> {
|
|
1371
1437
|
if (updateInfo) return updateInfo
|
|
@@ -1452,7 +1518,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1452
1518
|
} catch { /* MCP failure notifications are best-effort */ }
|
|
1453
1519
|
}
|
|
1454
1520
|
|
|
1455
|
-
async function notifyIfDuplicateMcps() {
|
|
1521
|
+
async function notifyIfDuplicateMcps() {
|
|
1456
1522
|
try {
|
|
1457
1523
|
if (!duplicateMcpServers.length) return
|
|
1458
1524
|
const label = duplicateMcpServers.length === 1 ? duplicateMcpServers[0] : `${duplicateMcpServers.length} MCPs`
|
|
@@ -1464,9 +1530,242 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1464
1530
|
duration: 12000,
|
|
1465
1531
|
},
|
|
1466
1532
|
})
|
|
1467
|
-
} catch { /* duplicate MCP notifications are best-effort */ }
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1533
|
+
} catch { /* duplicate MCP notifications are best-effort */ }
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function delegationAgent(kind: DelegationKind): "raven" | "raven-mcp" {
|
|
1537
|
+
return kind === "mcp" ? "raven-mcp" : "raven"
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
function delegationToolName(kind: DelegationKind): "raven_seek" | "raven_mcp" {
|
|
1541
|
+
return kind === "mcp" ? "raven_mcp" : "raven_seek"
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
function delegationTitle(kind: DelegationKind): string {
|
|
1545
|
+
return kind === "mcp" ? "Raven MCP" : "Raven Seek"
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
async function rootSessionId(sessionId: string, signal?: AbortSignal): Promise<string> {
|
|
1549
|
+
let current = sessionId
|
|
1550
|
+
const seen = new Set<string>()
|
|
1551
|
+
while (!seen.has(current)) {
|
|
1552
|
+
seen.add(current)
|
|
1553
|
+
const response = await client.session.get({ path: { id: current }, signal })
|
|
1554
|
+
const session = (response as any)?.data ?? response
|
|
1555
|
+
if (!session?.parentID) return current
|
|
1556
|
+
current = session.parentID
|
|
1557
|
+
}
|
|
1558
|
+
return sessionId
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
async function executeRavenDelegation(kind: DelegationKind, args: { query: string; sessionId?: string }, context: any) {
|
|
1562
|
+
const started = Date.now()
|
|
1563
|
+
const agent = delegationAgent(kind)
|
|
1564
|
+
const toolName = delegationToolName(kind)
|
|
1565
|
+
const title = delegationTitle(kind)
|
|
1566
|
+
const timeout = (kind === "mcp" ? config.raven_mcp.timeout : config.raven_seek.timeout) ?? 600
|
|
1567
|
+
const timeoutMs = timeout * 1000
|
|
1568
|
+
let sessionId: string | undefined
|
|
1569
|
+
let resumed = false
|
|
1570
|
+
let previousSavedCandidate = 0
|
|
1571
|
+
|
|
1572
|
+
try {
|
|
1573
|
+
const requestedSessionId = args.sessionId?.trim()
|
|
1574
|
+
if (args.sessionId !== undefined && !requestedSessionId) {
|
|
1575
|
+
return { title, output: "Cannot resume Raven: sessionId must not be empty." }
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
let rootId = context.sessionID
|
|
1579
|
+
try {
|
|
1580
|
+
rootId = await rootSessionId(context.sessionID, context.abort)
|
|
1581
|
+
} catch (error) {
|
|
1582
|
+
if (context.abort.aborted) throw error
|
|
1583
|
+
// Falling back preserves delegation if an older OpenCode build cannot read the caller session.
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
if (requestedSessionId) {
|
|
1587
|
+
const knownKind = ravenSessionKinds.get(requestedSessionId)
|
|
1588
|
+
const knownRavenSession = knownKind === kind || sessionAgents.get(requestedSessionId) === agent
|
|
1589
|
+
let existingSession: any
|
|
1590
|
+
try {
|
|
1591
|
+
const sessionResp = await client.session.get({ path: { id: requestedSessionId }, signal: context.abort })
|
|
1592
|
+
existingSession = (sessionResp as any)?.data ?? sessionResp
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
if (context.abort.aborted) throw error
|
|
1595
|
+
return { title, output: `Cannot resume Raven session ${requestedSessionId}: the session was not found or could not be read.` }
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
if (!existingSession?.id || existingSession.id !== requestedSessionId) {
|
|
1599
|
+
return { title, output: `Cannot resume Raven session ${requestedSessionId}: the session was not found or could not be read.` }
|
|
1600
|
+
}
|
|
1601
|
+
const validParent = existingSession.parentID === rootId || existingSession.parentID === context.sessionID
|
|
1602
|
+
if (!validParent || existingSession.projectID !== input.project.id) {
|
|
1603
|
+
return { title, output: "Cannot resume Raven: sessionId is not part of this main session's Raven tree." }
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
let messages: any[]
|
|
1607
|
+
try {
|
|
1608
|
+
const messagesResp = await client.session.messages({ path: { id: requestedSessionId }, signal: context.abort })
|
|
1609
|
+
const messageData = (messagesResp as any)?.data
|
|
1610
|
+
if (!Array.isArray(messageData)) {
|
|
1611
|
+
return { title, output: `Cannot resume Raven session ${requestedSessionId}: its message history could not be read.` }
|
|
1612
|
+
}
|
|
1613
|
+
messages = messageData
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
if (context.abort.aborted) throw error
|
|
1616
|
+
return { title, output: `Cannot resume Raven session ${requestedSessionId}: its message history could not be read.` }
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
const userMessages = messages.filter((message: any) => message?.info?.role === "user")
|
|
1620
|
+
const persistedRavenSession = userMessages.length > 0
|
|
1621
|
+
&& userMessages.every((message: any) => message.info.agent === agent)
|
|
1622
|
+
if (!persistedRavenSession && !(knownRavenSession && userMessages.length === 0)) {
|
|
1623
|
+
return { title, output: `Cannot resume Raven: sessionId does not belong to the ${agent} agent.` }
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
sessionId = requestedSessionId
|
|
1627
|
+
resumed = true
|
|
1628
|
+
previousSavedCandidate = estimateRavenSavedCandidateBytes(messages)
|
|
1629
|
+
} else {
|
|
1630
|
+
const session = await client.session.create({
|
|
1631
|
+
body: {
|
|
1632
|
+
parentID: rootId,
|
|
1633
|
+
title: `${toolName}: ${args.query.slice(0, 80)}`,
|
|
1634
|
+
},
|
|
1635
|
+
signal: context.abort,
|
|
1636
|
+
})
|
|
1637
|
+
sessionId = (session as any)?.data?.id ?? (session as any)?.id
|
|
1638
|
+
if (!sessionId) return { title, output: "Failed to create Raven session." }
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
ravenSessions.add(sessionId)
|
|
1642
|
+
ravenSessionKinds.set(sessionId, kind)
|
|
1643
|
+
const activeSessionId = sessionId
|
|
1644
|
+
context.metadata({ metadata: { sessionId } })
|
|
1645
|
+
|
|
1646
|
+
try {
|
|
1647
|
+
writeFileSync(join(tmpdir(), "raven-sessions.log"), `${new Date().toISOString()} ${sessionId}\n`)
|
|
1648
|
+
} catch { /* non-fatal */ }
|
|
1649
|
+
|
|
1650
|
+
if (activeRavenPrompts.has(activeSessionId)) {
|
|
1651
|
+
return {
|
|
1652
|
+
title,
|
|
1653
|
+
metadata: { sessionId: activeSessionId },
|
|
1654
|
+
output: `Raven session \`${activeSessionId}\` is already handling a request. Wait for it to finish before sending another follow-up.`,
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
activeRavenPrompts.add(activeSessionId)
|
|
1658
|
+
|
|
1659
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
1660
|
+
let promptCompleted = false
|
|
1661
|
+
let sessionAbort: Promise<unknown> | undefined
|
|
1662
|
+
const promptController = new AbortController()
|
|
1663
|
+
const abortPrompt = () => {
|
|
1664
|
+
if (promptController.signal.aborted) return
|
|
1665
|
+
promptController.abort(context.abort.reason)
|
|
1666
|
+
const abortController = new AbortController()
|
|
1667
|
+
const abortTimer = setTimeout(() => abortController.abort(), Math.min(timeoutMs, 5_000))
|
|
1668
|
+
sessionAbort = client.session.abort({
|
|
1669
|
+
path: { id: activeSessionId },
|
|
1670
|
+
signal: abortController.signal,
|
|
1671
|
+
throwOnError: true,
|
|
1672
|
+
}).catch(() => undefined).finally(() => clearTimeout(abortTimer))
|
|
1673
|
+
}
|
|
1674
|
+
if (context.abort.aborted) abortPrompt()
|
|
1675
|
+
else context.abort.addEventListener("abort", abortPrompt, { once: true })
|
|
1676
|
+
const providerError = waitForRavenProviderError(sessionId)
|
|
1677
|
+
const firstProgress = waitForRavenFirstProgress(sessionId, Math.min(timeoutMs, 30_000))
|
|
1678
|
+
|
|
1679
|
+
try {
|
|
1680
|
+
const result = await Promise.race([
|
|
1681
|
+
client.session.prompt({
|
|
1682
|
+
path: { id: sessionId },
|
|
1683
|
+
body: { agent, parts: [{ type: "text", text: args.query }] },
|
|
1684
|
+
signal: promptController.signal,
|
|
1685
|
+
throwOnError: true,
|
|
1686
|
+
}),
|
|
1687
|
+
providerError,
|
|
1688
|
+
firstProgress,
|
|
1689
|
+
new Promise<never>((_, reject) => {
|
|
1690
|
+
timer = setTimeout(() => {
|
|
1691
|
+
abortPrompt()
|
|
1692
|
+
reject(new Error(`Raven timed out after ${timeout}s - child session retained: ${sessionId}`))
|
|
1693
|
+
}, timeoutMs)
|
|
1694
|
+
}),
|
|
1695
|
+
])
|
|
1696
|
+
promptCompleted = true
|
|
1697
|
+
if (timer) clearTimeout(timer)
|
|
1698
|
+
clearRavenWaiters(sessionId)
|
|
1699
|
+
if ((result as any)?.error) throw new Error(providerErrorMessage((result as any).error))
|
|
1700
|
+
|
|
1701
|
+
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
1702
|
+
const parts = (result as any)?.data?.parts ?? []
|
|
1703
|
+
const output = parts
|
|
1704
|
+
.filter((part: any) => part.type === "text" && part.text)
|
|
1705
|
+
.map((part: any) => part.text)
|
|
1706
|
+
.join("\n") || "Raven returned no results."
|
|
1707
|
+
|
|
1708
|
+
let savedCandidate = 0
|
|
1709
|
+
const statsController = new AbortController()
|
|
1710
|
+
const abortStats = () => statsController.abort(context.abort.reason)
|
|
1711
|
+
const statsTimer = setTimeout(() => statsController.abort(), Math.min(timeoutMs, 5_000))
|
|
1712
|
+
if (context.abort.aborted) abortStats()
|
|
1713
|
+
else context.abort.addEventListener("abort", abortStats, { once: true })
|
|
1714
|
+
try {
|
|
1715
|
+
const totalSavedCandidate = await countRavenSavedCandidateBytes(sessionId, statsController.signal)
|
|
1716
|
+
savedCandidate = Math.max(0, totalSavedCandidate - previousSavedCandidate)
|
|
1717
|
+
} catch { /* best-effort */ }
|
|
1718
|
+
finally {
|
|
1719
|
+
clearTimeout(statsTimer)
|
|
1720
|
+
context.abort.removeEventListener("abort", abortStats)
|
|
1721
|
+
}
|
|
1722
|
+
if (savedCandidate <= 0) {
|
|
1723
|
+
for (const part of parts) {
|
|
1724
|
+
if (part.args) savedCandidate += JSON.stringify(part.args).length
|
|
1725
|
+
if (part.content) savedCandidate += typeof part.content === "string" ? part.content.length : JSON.stringify(part.content).length
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
const continuation = `Raven session: \`${sessionId}\`${resumed ? " (continued)" : ""}. Pass this as \`sessionId\` in a later \`${toolName}\` call to continue the same retrieval.`
|
|
1730
|
+
const activity = kind === "mcp" ? "used MCPs" : "searched"
|
|
1731
|
+
const resultOutput = `${output}\n\n*Raven ${activity} for ${elapsed}s - ${formatBytes(savedCandidate)} handled, ${formatTokens(savedCandidate)} tokens*\n\n${continuation}`
|
|
1732
|
+
addBytes(Math.max(0, savedCandidate - resultOutput.length))
|
|
1733
|
+
delete ravenProviderIssues[kind]
|
|
1734
|
+
return { title, metadata: { sessionId }, output: resultOutput }
|
|
1735
|
+
} finally {
|
|
1736
|
+
context.abort.removeEventListener("abort", abortPrompt)
|
|
1737
|
+
if (!promptCompleted) abortPrompt()
|
|
1738
|
+
if (sessionAbort) await sessionAbort
|
|
1739
|
+
if (timer) clearTimeout(timer)
|
|
1740
|
+
clearRavenWaiters(sessionId)
|
|
1741
|
+
activeRavenPrompts.delete(activeSessionId)
|
|
1742
|
+
}
|
|
1743
|
+
} catch (error: any) {
|
|
1744
|
+
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
1745
|
+
const message = String(error?.message ?? error ?? "").toLowerCase()
|
|
1746
|
+
const missingSession = /session.*not found|not found.*session/i.test(message)
|
|
1747
|
+
const activity = kind === "mcp" ? "MCP retrieval" : "search"
|
|
1748
|
+
const hint =
|
|
1749
|
+
context.abort.aborted ? `Raven ${activity} cancelled.`
|
|
1750
|
+
: /rate.?limit|too many requests|429/i.test(message) ? "Raven rate limited - wait 30s, then retry with a narrower query."
|
|
1751
|
+
: /quota|usage.?limit|usage exceeded|free usage|subscribe to go|subscription|billing|insufficient.*(?:credit|balance|quota)/i.test(message) ? "Raven API quota/subscription exhausted - proceed without Raven and mention what information may be missing."
|
|
1752
|
+
: /auth|unauthorized|forbidden|401|403|api key|login/i.test(message) ? "Raven provider authentication failed - proceed without Raven and check the configured provider login or API key."
|
|
1753
|
+
: /token|context.?length|too large|too long/i.test(message) ? "Raven query too large - shorten your query and retry."
|
|
1754
|
+
: missingSession ? `The Raven session is no longer available - retry without sessionId to start a new ${activity}.`
|
|
1755
|
+
: /model|unavailable|down|not found/i.test(message) ? "Raven model unavailable - retry later, or proceed without the missing information."
|
|
1756
|
+
: /did not produce an initial response|retry dialog|timeout|timed.?out|session retained/i.test(message) ? error.message
|
|
1757
|
+
: `Raven ${activity} failed. Note the resulting information gap. [${error.message || error}]`
|
|
1758
|
+
if (!missingSession && /quota|usage.?limit|usage exceeded|free usage|subscribe to go|subscription|billing|auth|unauthorized|forbidden|401|403|api key|login|model|unavailable|down|not found|did not produce an initial response|retry dialog/i.test(message)) {
|
|
1759
|
+
ravenProviderIssues[kind] = hint
|
|
1760
|
+
}
|
|
1761
|
+
const recovery = ravenProviderIssues[kind] ? `\n\n${ravenRecoveryInstructions(kind)}` : ""
|
|
1762
|
+
const continuation = sessionId && !missingSession
|
|
1763
|
+
? `\n\nRaven session: \`${sessionId}\`. Pass this as \`sessionId\` in a later \`${toolName}\` call to retry or continue.`
|
|
1764
|
+
: ""
|
|
1765
|
+
return { title, metadata: sessionId ? { sessionId } : undefined, output: `${hint}${recovery}\n\n*Attempt took ${elapsed}s*${continuation}` }
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1470
1769
|
return {
|
|
1471
1770
|
async dispose() {
|
|
1472
1771
|
for (const controller of mcpRefreshControllers) controller.abort()
|
|
@@ -1501,26 +1800,38 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1501
1800
|
|
|
1502
1801
|
// Register Raven from Raven.md, with config file overrides
|
|
1503
1802
|
configInput.agent = configInput.agent || {}
|
|
1504
|
-
configInput.agent.raven = {
|
|
1505
|
-
description: fm.description || "",
|
|
1506
|
-
mode: fm.mode || "subagent",
|
|
1507
|
-
hidden: fm.hidden !== undefined ? fm.hidden : false,
|
|
1508
|
-
model: config.model || fm.model,
|
|
1509
|
-
options: {
|
|
1510
|
-
...extractOptions(fm),
|
|
1511
|
-
...(config.reasoning_effort ? { reasoning_effort: config.reasoning_effort } : {}),
|
|
1512
|
-
},
|
|
1513
|
-
permission: fm.permission || {},
|
|
1514
|
-
prompt: ravenAgentPrompt(),
|
|
1515
|
-
}
|
|
1803
|
+
configInput.agent.raven = {
|
|
1804
|
+
description: fm.description || "",
|
|
1805
|
+
mode: fm.mode || "subagent",
|
|
1806
|
+
hidden: fm.hidden !== undefined ? fm.hidden : false,
|
|
1807
|
+
model: config.raven_seek.model || fm.model,
|
|
1808
|
+
options: {
|
|
1809
|
+
...extractOptions(fm),
|
|
1810
|
+
...(config.raven_seek.reasoning_effort ? { reasoning_effort: config.raven_seek.reasoning_effort } : {}),
|
|
1811
|
+
},
|
|
1812
|
+
permission: fm.permission || {},
|
|
1813
|
+
prompt: ravenAgentPrompt(),
|
|
1814
|
+
}
|
|
1815
|
+
configInput.agent["raven-mcp"] = {
|
|
1816
|
+
description: mcpFm.description || "",
|
|
1817
|
+
mode: mcpFm.mode || "subagent",
|
|
1818
|
+
hidden: mcpFm.hidden !== undefined ? mcpFm.hidden : true,
|
|
1819
|
+
model: config.raven_mcp.model || mcpFm.model,
|
|
1820
|
+
options: {
|
|
1821
|
+
...extractOptions(mcpFm),
|
|
1822
|
+
...(config.raven_mcp.reasoning_effort ? { reasoning_effort: config.raven_mcp.reasoning_effort } : {}),
|
|
1823
|
+
},
|
|
1824
|
+
permission: mcpFm.permission || {},
|
|
1825
|
+
prompt: ravenMcpAgentPrompt(),
|
|
1826
|
+
}
|
|
1516
1827
|
|
|
1517
1828
|
// Register /raven command
|
|
1518
1829
|
configInput.command = configInput.command || {}
|
|
1519
1830
|
if (!configInput.command.raven) {
|
|
1520
|
-
configInput.command.raven = {
|
|
1521
|
-
template: "Manage Raven
|
|
1522
|
-
description: "
|
|
1523
|
-
}
|
|
1831
|
+
configInput.command.raven = {
|
|
1832
|
+
template: "Manage Raven search routing, on-demand MCPs, models, timeouts, updates, and status",
|
|
1833
|
+
description: "Manage Raven search routing, on-demand MCPs, and per-agent models",
|
|
1834
|
+
}
|
|
1524
1835
|
}
|
|
1525
1836
|
|
|
1526
1837
|
scheduleOnDemandMcpRefresh()
|
|
@@ -1529,33 +1840,93 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1529
1840
|
duplicateMcpToastPending = duplicateMcpServers.length > 0
|
|
1530
1841
|
},
|
|
1531
1842
|
|
|
1532
|
-
// Register raven_seek tool — lets agents with task:false still delegate through Raven
|
|
1533
|
-
tool: {
|
|
1534
|
-
"raven_seek": tool({
|
|
1535
|
-
description: "
|
|
1536
|
-
args: {
|
|
1537
|
-
query: tool.schema.string().describe("
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
const
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1843
|
+
// Register raven_seek tool — lets agents with task:false still delegate through Raven
|
|
1844
|
+
tool: {
|
|
1845
|
+
"raven_seek": tool({
|
|
1846
|
+
description: "Search and evidence retrieval only. Creates or resumes a Raven Search session for routed local search, filesystem discovery, URL fetching, web/docs/GitHub search, globally configured MCP searches, or command-output inspection. Do not delegate code review, auditing, correctness judgments, implementation, debugging decisions, planning, or general task execution; the calling agent must perform that work.",
|
|
1847
|
+
args: {
|
|
1848
|
+
query: tool.schema.string().describe("A narrowly scoped search or evidence-retrieval request. Include original blocked tool args, exact URLs, paths, patterns, or bounded command-output checks. Do not ask Raven to review or audit code."),
|
|
1849
|
+
sessionId: tool.schema.string().optional().describe("Raven session ID returned by an earlier raven_seek call. Provide it only when this request should continue that session's research context."),
|
|
1850
|
+
},
|
|
1851
|
+
async execute(args, context) {
|
|
1852
|
+
const started = Date.now()
|
|
1853
|
+
const timeout = (config.raven_seek.timeout ?? 600) * 1000
|
|
1854
|
+
let sessionId: string | undefined
|
|
1855
|
+
let resumed = false
|
|
1856
|
+
let previousSavedCandidate = 0
|
|
1857
|
+
try {
|
|
1858
|
+
const requestedSessionId = args.sessionId?.trim()
|
|
1859
|
+
if (args.sessionId !== undefined && !requestedSessionId) {
|
|
1860
|
+
return { title: "Raven Seek", output: "Cannot resume Raven: sessionId must not be empty." }
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
let rootId = context.sessionID
|
|
1864
|
+
try {
|
|
1865
|
+
rootId = await rootSessionId(context.sessionID, context.abort)
|
|
1866
|
+
} catch (error) {
|
|
1867
|
+
if (context.abort.aborted) throw error
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
if (requestedSessionId) {
|
|
1871
|
+
const knownRavenSession = ravenSessionKinds.get(requestedSessionId) === "seek" || sessionAgents.get(requestedSessionId) === "raven"
|
|
1872
|
+
let existingSession: any
|
|
1873
|
+
try {
|
|
1874
|
+
const sessionResp = await client.session.get({ path: { id: requestedSessionId }, signal: context.abort })
|
|
1875
|
+
existingSession = (sessionResp as any)?.data ?? sessionResp
|
|
1876
|
+
} catch (err) {
|
|
1877
|
+
if (context.abort.aborted) throw err
|
|
1878
|
+
return { title: "Raven Seek", output: `Cannot resume Raven session ${requestedSessionId}: the session was not found or could not be read.` }
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
if (!existingSession?.id || existingSession.id !== requestedSessionId) {
|
|
1882
|
+
return { title: "Raven Seek", output: `Cannot resume Raven session ${requestedSessionId}: the session was not found or could not be read.` }
|
|
1883
|
+
}
|
|
1884
|
+
if ((existingSession.parentID !== rootId && existingSession.parentID !== context.sessionID) || existingSession.projectID !== input.project.id) {
|
|
1885
|
+
return { title: "Raven Seek", output: "Cannot resume Raven: sessionId is not a Raven child of this main session." }
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
let messages: any[]
|
|
1889
|
+
try {
|
|
1890
|
+
const messagesResp = await client.session.messages({ path: { id: requestedSessionId }, signal: context.abort })
|
|
1891
|
+
const messageData = (messagesResp as any)?.data
|
|
1892
|
+
if (!Array.isArray(messageData)) {
|
|
1893
|
+
return { title: "Raven Seek", output: `Cannot resume Raven session ${requestedSessionId}: its message history could not be read.` }
|
|
1894
|
+
}
|
|
1895
|
+
messages = messageData
|
|
1896
|
+
} catch (err) {
|
|
1897
|
+
if (context.abort.aborted) throw err
|
|
1898
|
+
return { title: "Raven Seek", output: `Cannot resume Raven session ${requestedSessionId}: its message history could not be read.` }
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
const userMessages = messages.filter((message: any) => message?.info?.role === "user")
|
|
1902
|
+
const persistedRavenSession = userMessages.length > 0
|
|
1903
|
+
&& userMessages.every((message: any) => message.info.agent === "raven")
|
|
1904
|
+
if (!persistedRavenSession && !(knownRavenSession && userMessages.length === 0)) {
|
|
1905
|
+
return { title: "Raven Seek", output: "Cannot resume Raven: sessionId is not a Raven child of this main session." }
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
sessionId = requestedSessionId
|
|
1909
|
+
resumed = true
|
|
1910
|
+
previousSavedCandidate = estimateRavenSavedCandidateBytes(messages)
|
|
1911
|
+
} else {
|
|
1912
|
+
const session = await client.session.create({
|
|
1913
|
+
body: {
|
|
1914
|
+
parentID: rootId,
|
|
1915
|
+
title: `raven_seek: ${args.query.slice(0, 80)}`,
|
|
1916
|
+
},
|
|
1917
|
+
signal: context.abort,
|
|
1918
|
+
})
|
|
1919
|
+
|
|
1920
|
+
sessionId = (session as any)?.data?.id ?? (session as any)?.id
|
|
1921
|
+
if (!sessionId) {
|
|
1922
|
+
return { title: "Raven Seek", output: "Failed to create Raven session." }
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
ravenSessions.add(sessionId)
|
|
1927
|
+
ravenSessionKinds.set(sessionId, "seek")
|
|
1928
|
+
const activeSessionId = sessionId
|
|
1929
|
+
|
|
1559
1930
|
// Preserve the child session link for TUI clients that render it.
|
|
1560
1931
|
context.metadata({ metadata: { sessionId } })
|
|
1561
1932
|
|
|
@@ -1564,15 +1935,35 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1564
1935
|
const logFile = join(tmpdir(), "raven-sessions.log")
|
|
1565
1936
|
const ts = new Date().toISOString()
|
|
1566
1937
|
writeFileSync(logFile, `${ts} ${sessionId}\n`)
|
|
1567
|
-
} catch { /* non-fatal */ }
|
|
1938
|
+
} catch { /* non-fatal */ }
|
|
1939
|
+
|
|
1940
|
+
if (activeRavenPrompts.has(activeSessionId)) {
|
|
1941
|
+
return {
|
|
1942
|
+
title: "Raven Seek",
|
|
1943
|
+
metadata: { sessionId: activeSessionId },
|
|
1944
|
+
output: `Raven session \`${activeSessionId}\` is already handling a request. Wait for it to finish before sending another follow-up.`,
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
activeRavenPrompts.add(activeSessionId)
|
|
1568
1948
|
|
|
1569
1949
|
// Send the query to Raven with timeout. Provider/API errors are also
|
|
1570
1950
|
// surfaced through message.updated events; fail as soon as one lands
|
|
1571
1951
|
// so the caller does not wait for the full timeout on quota/auth issues.
|
|
1572
1952
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
1573
1953
|
let promptCompleted = false
|
|
1954
|
+
let sessionAbort: Promise<unknown> | undefined
|
|
1574
1955
|
const promptController = new AbortController()
|
|
1575
|
-
const abortPrompt = () =>
|
|
1956
|
+
const abortPrompt = () => {
|
|
1957
|
+
if (promptController.signal.aborted) return
|
|
1958
|
+
promptController.abort(context.abort.reason)
|
|
1959
|
+
const abortController = new AbortController()
|
|
1960
|
+
const abortTimer = setTimeout(() => abortController.abort(), Math.min(timeout, 5_000))
|
|
1961
|
+
sessionAbort = client.session.abort({
|
|
1962
|
+
path: { id: activeSessionId },
|
|
1963
|
+
signal: abortController.signal,
|
|
1964
|
+
throwOnError: true,
|
|
1965
|
+
}).catch(() => undefined).finally(() => clearTimeout(abortTimer))
|
|
1966
|
+
}
|
|
1576
1967
|
if (context.abort.aborted) abortPrompt()
|
|
1577
1968
|
else context.abort.addEventListener("abort", abortPrompt, { once: true })
|
|
1578
1969
|
const providerError = waitForRavenProviderError(sessionId)
|
|
@@ -1586,12 +1977,13 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1586
1977
|
parts: [{ type: "text", text: args.query }],
|
|
1587
1978
|
},
|
|
1588
1979
|
signal: promptController.signal,
|
|
1980
|
+
throwOnError: true,
|
|
1589
1981
|
}),
|
|
1590
1982
|
providerError,
|
|
1591
1983
|
firstProgress,
|
|
1592
1984
|
new Promise<never>((_, reject) => {
|
|
1593
1985
|
timer = setTimeout(() => {
|
|
1594
|
-
|
|
1986
|
+
abortPrompt()
|
|
1595
1987
|
reject(new Error(`Raven timed out after ${timeout / 1000}s — child session retained: ${sessionId}`))
|
|
1596
1988
|
}, timeout)
|
|
1597
1989
|
}),
|
|
@@ -1599,8 +1991,9 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1599
1991
|
promptCompleted = true
|
|
1600
1992
|
if (timer) clearTimeout(timer)
|
|
1601
1993
|
clearRavenWaiters(sessionId)
|
|
1994
|
+
if ((result as any)?.error) throw new Error(providerErrorMessage((result as any).error))
|
|
1602
1995
|
|
|
1603
|
-
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
1996
|
+
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
1604
1997
|
|
|
1605
1998
|
// Extract text from the response
|
|
1606
1999
|
const parts = (result as any)?.data?.parts ?? []
|
|
@@ -1609,51 +2002,79 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1609
2002
|
.map((p: any) => p.text)
|
|
1610
2003
|
const output = textParts.join("\n") || "Raven returned no results."
|
|
1611
2004
|
|
|
1612
|
-
// Context saved = Raven output/reasoning/cache context - compact answer returned to main session.
|
|
1613
|
-
let savedCandidate = 0
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
2005
|
+
// Context saved = Raven output/reasoning/cache context - compact answer returned to main session.
|
|
2006
|
+
let savedCandidate = 0
|
|
2007
|
+
const statsController = new AbortController()
|
|
2008
|
+
const abortStats = () => statsController.abort(context.abort.reason)
|
|
2009
|
+
const statsTimer = setTimeout(() => statsController.abort(), Math.min(timeout, 5_000))
|
|
2010
|
+
if (context.abort.aborted) abortStats()
|
|
2011
|
+
else context.abort.addEventListener("abort", abortStats, { once: true })
|
|
2012
|
+
try {
|
|
2013
|
+
const totalSavedCandidate = await countRavenSavedCandidateBytes(sessionId, statsController.signal)
|
|
2014
|
+
savedCandidate = Math.max(0, totalSavedCandidate - previousSavedCandidate)
|
|
2015
|
+
} catch { /* best-effort */ }
|
|
2016
|
+
finally {
|
|
2017
|
+
clearTimeout(statsTimer)
|
|
2018
|
+
context.abort.removeEventListener("abort", abortStats)
|
|
2019
|
+
}
|
|
1617
2020
|
if (savedCandidate <= 0) {
|
|
1618
2021
|
for (const part of parts) {
|
|
1619
2022
|
if (part.args) savedCandidate += JSON.stringify(part.args).length
|
|
1620
2023
|
if (part.content) savedCandidate += typeof part.content === "string" ? part.content.length : JSON.stringify(part.content).length
|
|
1621
2024
|
}
|
|
1622
2025
|
}
|
|
1623
|
-
const
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
2026
|
+
const continuation = `Raven session: \`${sessionId}\`${resumed ? " (continued)" : ""}. Pass this as \`sessionId\` in a later \`raven_seek\` call to continue the same research.`
|
|
2027
|
+
const resultOutput = `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(savedCandidate)} handled, ${formatTokens(savedCandidate)} tokens*\n\n${continuation}`
|
|
2028
|
+
addBytes(Math.max(0, savedCandidate - resultOutput.length))
|
|
2029
|
+
delete ravenProviderIssues.seek
|
|
2030
|
+
|
|
2031
|
+
return { title: "Raven Seek", metadata: { sessionId }, output: resultOutput }
|
|
1628
2032
|
} finally {
|
|
1629
2033
|
context.abort.removeEventListener("abort", abortPrompt)
|
|
1630
|
-
if (!promptCompleted)
|
|
2034
|
+
if (!promptCompleted) abortPrompt()
|
|
2035
|
+
if (sessionAbort) await sessionAbort
|
|
1631
2036
|
if (timer) clearTimeout(timer)
|
|
1632
2037
|
clearRavenWaiters(sessionId)
|
|
2038
|
+
activeRavenPrompts.delete(activeSessionId)
|
|
1633
2039
|
}
|
|
1634
|
-
} catch (err: any) {
|
|
1635
|
-
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
2040
|
+
} catch (err: any) {
|
|
2041
|
+
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
1636
2042
|
const msg = String(err?.message ?? err ?? "").toLowerCase()
|
|
2043
|
+
const missingSession = /session.*not found|not found.*session/i.test(msg)
|
|
1637
2044
|
const hint =
|
|
1638
2045
|
context.abort.aborted ? "Raven search cancelled."
|
|
1639
2046
|
: /rate.?limit|too many requests|429/i.test(msg) ? "Raven rate limited — wait 30s then retry with a narrower query."
|
|
1640
|
-
: /quota|usage.?limit|usage exceeded|free usage|subscribe to go|subscription|billing|insufficient.*(?:credit|balance|quota)/i.test(msg) ? "Raven API quota/subscription exhausted — proceed without Raven, tell the user Raven could not run, and mention what information may be missing."
|
|
1641
|
-
: /auth|unauthorized|forbidden|401|403|api key|login/i.test(msg) ? "Raven provider authentication failed — proceed without Raven and tell the user to check the Raven model/provider login or API key."
|
|
1642
|
-
: /token|context.?length|too large|too long/i.test(msg) ? "Raven query too large — shorten your query and retry."
|
|
1643
|
-
:
|
|
1644
|
-
: /
|
|
1645
|
-
: /
|
|
1646
|
-
:
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
2047
|
+
: /quota|usage.?limit|usage exceeded|free usage|subscribe to go|subscription|billing|insufficient.*(?:credit|balance|quota)/i.test(msg) ? "Raven API quota/subscription exhausted — proceed without Raven, tell the user Raven could not run, and mention what information may be missing."
|
|
2048
|
+
: /auth|unauthorized|forbidden|401|403|api key|login/i.test(msg) ? "Raven provider authentication failed — proceed without Raven and tell the user to check the Raven model/provider login or API key."
|
|
2049
|
+
: /token|context.?length|too large|too long/i.test(msg) ? "Raven query too large — shorten your query and retry."
|
|
2050
|
+
: missingSession ? "The Raven session is no longer available — retry without sessionId to start a new Raven search."
|
|
2051
|
+
: /model|unavailable|down|not found/i.test(msg) ? "Raven model unavailable — retry later, or proceed without search."
|
|
2052
|
+
: /did not produce an initial response|retry dialog/i.test(msg) ? err.message
|
|
2053
|
+
: /timeout|timed.?out|session kept/i.test(msg) ? err.message
|
|
2054
|
+
: `Raven search failed. Proceed without search — note gaps for the user. [${err.message || err}]`
|
|
2055
|
+
if (!missingSession && /quota|usage.?limit|usage exceeded|free usage|subscribe to go|subscription|billing|auth|unauthorized|forbidden|401|403|api key|login|model|unavailable|down|not found|did not produce an initial response|retry dialog/i.test(msg)) {
|
|
2056
|
+
ravenProviderIssues.seek = hint
|
|
2057
|
+
}
|
|
2058
|
+
const recovery = ravenProviderIssues.seek ? `\n\n${ravenRecoveryInstructions("seek")}` : ""
|
|
2059
|
+
const continuation = sessionId && !missingSession
|
|
2060
|
+
? `\n\nRaven session: \`${sessionId}\`. Pass this as \`sessionId\` in a later \`raven_seek\` call to retry or continue.`
|
|
2061
|
+
: ""
|
|
2062
|
+
return { title: "Raven Seek", metadata: sessionId ? { sessionId } : undefined, output: `${hint}${recovery}\n\n*Attempt took ${elapsed}s*${continuation}` }
|
|
1652
2063
|
}
|
|
1653
2064
|
},
|
|
1654
2065
|
}),
|
|
1655
|
-
"raven_mcp": tool({
|
|
1656
|
-
description: "
|
|
2066
|
+
"raven_mcp": tool({
|
|
2067
|
+
description: "On-demand MCP information retrieval only. Creates or resumes a dedicated Raven MCP session that selects and calls MCPs configured under raven_mcp.onDemandMcpServers. Use for matching MCP capabilities, not local search, code review, auditing, implementation, planning, or general task delegation.",
|
|
2068
|
+
args: {
|
|
2069
|
+
query: tool.schema.string().describe("The information to retrieve and, when known, the target MCP server or capability. The calling agent retains analysis and judgment."),
|
|
2070
|
+
sessionId: tool.schema.string().optional().describe("Raven MCP session ID returned by an earlier raven_mcp call. Provide it only to continue that MCP retrieval context."),
|
|
2071
|
+
},
|
|
2072
|
+
async execute(args, context) {
|
|
2073
|
+
return executeRavenDelegation("mcp", args, context)
|
|
2074
|
+
},
|
|
2075
|
+
}),
|
|
2076
|
+
"raven_mcp_bridge": tool({
|
|
2077
|
+
description: "Internal bridge for the Raven MCP agent. Other agents must not call this tool. Lists and calls tools, resources, and prompts from configured on-demand MCP servers without globally exposing their schemas.",
|
|
1657
2078
|
args: {
|
|
1658
2079
|
server: tool.schema.string().describe("On-demand MCP server name from Raven config, e.g. context7, exa, grep_app, unityMCP."),
|
|
1659
2080
|
operation: tool.schema.enum(["list_tools", "call_tool", "list_resources", "read_resource", "list_prompts", "get_prompt"]).describe("MCP operation to perform."),
|
|
@@ -1661,13 +2082,13 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1661
2082
|
argumentsJson: tool.schema.string().optional().describe("JSON object string with arguments for call_tool or get_prompt, e.g. {\"query\":\"latest AI news\"}. Omit for no arguments."),
|
|
1662
2083
|
},
|
|
1663
2084
|
async execute(args, context) {
|
|
1664
|
-
if (
|
|
1665
|
-
return { title: "Raven MCP", output: "
|
|
2085
|
+
if (ravenSessionKinds.get(context.sessionID) !== "mcp" && sessionAgents.get(context.sessionID) !== "raven-mcp") {
|
|
2086
|
+
return { title: "Raven MCP Bridge", output: "raven_mcp_bridge is only available inside the Raven MCP agent. Use raven_mcp for on-demand MCP requests." }
|
|
1666
2087
|
}
|
|
1667
2088
|
|
|
1668
2089
|
const operation = args.operation as RavenMcpOperation
|
|
1669
2090
|
const server = getOnDemandMcpServer(args.server)?.[1]
|
|
1670
|
-
const timeout = server ? mcpTimeoutMs(server) : (config.timeout ?? 600) * 1000
|
|
2091
|
+
const timeout = server ? mcpTimeoutMs(server) : (config.raven_mcp.timeout ?? 600) * 1000
|
|
1671
2092
|
const requestOptions = { signal: context.abort, timeout }
|
|
1672
2093
|
let connected = false
|
|
1673
2094
|
let connection: McpConnection | undefined
|
|
@@ -1749,11 +2170,15 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1749
2170
|
|
|
1750
2171
|
// Track agent ↔ session mapping for allowlist + Raven exclusion
|
|
1751
2172
|
async "chat.message"(input: any, _output: any) {
|
|
1752
|
-
if (input.agent) {
|
|
1753
|
-
sessionAgents.set(input.sessionID, input.agent)
|
|
1754
|
-
if (input.agent === "raven") {
|
|
1755
|
-
ravenSessions.add(input.sessionID)
|
|
1756
|
-
|
|
2173
|
+
if (input.agent) {
|
|
2174
|
+
sessionAgents.set(input.sessionID, input.agent)
|
|
2175
|
+
if (input.agent === "raven") {
|
|
2176
|
+
ravenSessions.add(input.sessionID)
|
|
2177
|
+
ravenSessionKinds.set(input.sessionID, "seek")
|
|
2178
|
+
} else if (input.agent === "raven-mcp") {
|
|
2179
|
+
ravenSessions.add(input.sessionID)
|
|
2180
|
+
ravenSessionKinds.set(input.sessionID, "mcp")
|
|
2181
|
+
}
|
|
1757
2182
|
}
|
|
1758
2183
|
},
|
|
1759
2184
|
|
|
@@ -1768,6 +2193,8 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1768
2193
|
const sessionId = evt.properties?.info?.id
|
|
1769
2194
|
if (sessionId) {
|
|
1770
2195
|
ravenSessions.delete(sessionId)
|
|
2196
|
+
ravenSessionKinds.delete(sessionId)
|
|
2197
|
+
activeRavenPrompts.delete(sessionId)
|
|
1771
2198
|
sessionAgents.delete(sessionId)
|
|
1772
2199
|
clearRavenWaiters(sessionId)
|
|
1773
2200
|
for (const [parentId, childId] of ravenSessionParents) {
|
|
@@ -1822,22 +2249,16 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1822
2249
|
}
|
|
1823
2250
|
},
|
|
1824
2251
|
|
|
1825
|
-
// /raven help|
|
|
2252
|
+
// /raven help|route|seek ...|mcp ...|update|stats|status
|
|
1826
2253
|
async "command.execute.before"(input: any, output: any) {
|
|
1827
2254
|
if (input.command !== "raven") return
|
|
1828
2255
|
output.parts.length = 0
|
|
1829
2256
|
const raw = input.arguments.trim()
|
|
1830
2257
|
const arg = raw.toLowerCase()
|
|
1831
2258
|
|
|
1832
|
-
if (arg === "
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
} else if (arg === "help") {
|
|
1836
|
-
output.parts.push({ type: "text", text: helpText() })
|
|
1837
|
-
} else if (arg === "off") {
|
|
1838
|
-
updateConfig({ enabled: false })
|
|
1839
|
-
output.parts.push({ type: "text", text: "Raven tool/MCP routing disabled. All agents can use tools directly." })
|
|
1840
|
-
} else if (arg === "stats") {
|
|
2259
|
+
if (arg === "help") {
|
|
2260
|
+
output.parts.push({ type: "text", text: helpText() })
|
|
2261
|
+
} else if (arg === "stats") {
|
|
1841
2262
|
output.parts.push({ type: "text", text: statsText() })
|
|
1842
2263
|
} else if (arg === "route") {
|
|
1843
2264
|
output.parts.push({ type: "text", text: `${routeSummary()}\n\nUsage:\n /raven route tool add <tool_name>\n /raven route tool remove <tool_name>\n /raven route keyword add <keyword>\n /raven route keyword remove <keyword>\n\nGlobal MCPs are auto-routed by detected OpenCode MCP server name.` })
|
|
@@ -1852,16 +2273,38 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1852
2273
|
output.parts.push({ type: "text", text: "Usage:\n /raven route tool add <tool_name>\n /raven route tool remove <tool_name>\n /raven route keyword add <keyword>\n /raven route keyword remove <keyword>\n\nGlobal MCPs are auto-routed by detected OpenCode MCP server name." })
|
|
1853
2274
|
} else {
|
|
1854
2275
|
updateConfig((latest) => {
|
|
1855
|
-
const values = uniqueStrings(latest[key])
|
|
2276
|
+
const values = uniqueStrings(latest.raven_seek[key])
|
|
1856
2277
|
const exists = values.some((value) => value.toLowerCase() === name.toLowerCase())
|
|
1857
2278
|
const next = action === "add"
|
|
1858
2279
|
? exists ? values : [...values, name]
|
|
1859
2280
|
: values.filter((value) => value.toLowerCase() !== name.toLowerCase())
|
|
1860
|
-
return key === "routeTools" ? { routeTools: next } : { routeToolKeywords: next }
|
|
2281
|
+
return { raven_seek: key === "routeTools" ? { routeTools: next } : { routeToolKeywords: next } }
|
|
1861
2282
|
})
|
|
1862
2283
|
output.parts.push({ type: "text", text: routeSummary() })
|
|
1863
|
-
}
|
|
1864
|
-
} else if (arg
|
|
2284
|
+
}
|
|
2285
|
+
} else if (arg.startsWith("seek ")) {
|
|
2286
|
+
const parts = raw.split(/\s+/)
|
|
2287
|
+
const action = parts[1]?.toLowerCase()
|
|
2288
|
+
const value = parts.slice(2).join(" ").trim()
|
|
2289
|
+
if (action === "model" && value) {
|
|
2290
|
+
updateConfig({ raven_seek: { model: value } })
|
|
2291
|
+
delete ravenProviderIssues.seek
|
|
2292
|
+
output.parts.push({ type: "text", text: `Raven Search model set to: ${value}\nRestart opencode for the change to take effect.` })
|
|
2293
|
+
} else if (action === "effort" && value) {
|
|
2294
|
+
updateConfig({ raven_seek: { reasoning_effort: value } })
|
|
2295
|
+
output.parts.push({ type: "text", text: `Raven Search reasoning effort set to: ${value}\nRestart opencode for the change to take effect.` })
|
|
2296
|
+
} else if (action === "timeout") {
|
|
2297
|
+
const seconds = Number(value)
|
|
2298
|
+
if (!Number.isInteger(seconds) || seconds < 10 || seconds > MAX_TIMEOUT_SECONDS) {
|
|
2299
|
+
output.parts.push({ type: "text", text: `Usage: /raven seek timeout <seconds>\nMust be between 10 and ${MAX_TIMEOUT_SECONDS}. Current: ${config.raven_seek.timeout ?? 600}s` })
|
|
2300
|
+
} else {
|
|
2301
|
+
updateConfig({ raven_seek: { timeout: seconds } })
|
|
2302
|
+
output.parts.push({ type: "text", text: `Raven Search timeout set to ${seconds}s. Takes effect immediately.` })
|
|
2303
|
+
}
|
|
2304
|
+
} else {
|
|
2305
|
+
output.parts.push({ type: "text", text: "Usage:\n /raven seek model <provider/model>\n /raven seek effort <value>\n /raven seek timeout <seconds>" })
|
|
2306
|
+
}
|
|
2307
|
+
} else if (arg === "mcp") {
|
|
1865
2308
|
output.parts.push({ type: "text", text: `${onDemandMcpStatus()}\n\nUsage:\n /raven mcp refresh [server_name]\n /raven mcp detail full\n /raven mcp detail minimized` })
|
|
1866
2309
|
} else if (arg.startsWith("mcp ")) {
|
|
1867
2310
|
const parts = raw.split(/\s+/)
|
|
@@ -1874,16 +2317,33 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1874
2317
|
type: "text",
|
|
1875
2318
|
text: `On-demand MCP refresh complete.\n Refreshed: ${result.refreshed.length ? result.refreshed.join(", ") : "none"}\n Failed: ${result.failed.length ? result.failed.join(", ") : "none"}`,
|
|
1876
2319
|
})
|
|
1877
|
-
} else if (action === "detail") {
|
|
1878
|
-
const detail = parts[2]?.toLowerCase()
|
|
2320
|
+
} else if (action === "detail") {
|
|
2321
|
+
const detail = parts[2]?.toLowerCase()
|
|
1879
2322
|
if (detail !== "full" && detail !== "minimized") {
|
|
1880
|
-
output.parts.push({ type: "text", text: `Usage: /raven mcp detail full|minimized\nCurrent: ${config.
|
|
2323
|
+
output.parts.push({ type: "text", text: `Usage: /raven mcp detail full|minimized\nCurrent: ${config.raven_mcp.descriptionDetail ?? "full"}` })
|
|
1881
2324
|
} else {
|
|
1882
|
-
updateConfig({
|
|
2325
|
+
updateConfig({ raven_mcp: { descriptionDetail: detail } })
|
|
1883
2326
|
output.parts.push({ type: "text", text: `On-demand MCP guidance detail set to: ${detail}\nRestart opencode for startup instructions/tool descriptions to fully refresh.` })
|
|
1884
|
-
}
|
|
1885
|
-
} else {
|
|
1886
|
-
|
|
2327
|
+
}
|
|
2328
|
+
} else if (action === "model" && parts.slice(2).join(" ").trim()) {
|
|
2329
|
+
const model = parts.slice(2).join(" ").trim()
|
|
2330
|
+
updateConfig({ raven_mcp: { model } })
|
|
2331
|
+
delete ravenProviderIssues.mcp
|
|
2332
|
+
output.parts.push({ type: "text", text: `Raven MCP model set to: ${model}\nRestart opencode for the change to take effect.` })
|
|
2333
|
+
} else if (action === "effort" && parts.slice(2).join(" ").trim()) {
|
|
2334
|
+
const effort = parts.slice(2).join(" ").trim()
|
|
2335
|
+
updateConfig({ raven_mcp: { reasoning_effort: effort } })
|
|
2336
|
+
output.parts.push({ type: "text", text: `Raven MCP reasoning effort set to: ${effort}\nRestart opencode for the change to take effect.` })
|
|
2337
|
+
} else if (action === "timeout") {
|
|
2338
|
+
const seconds = Number(parts[2])
|
|
2339
|
+
if (!Number.isInteger(seconds) || seconds < 10 || seconds > MAX_TIMEOUT_SECONDS) {
|
|
2340
|
+
output.parts.push({ type: "text", text: `Usage: /raven mcp timeout <seconds>\nMust be between 10 and ${MAX_TIMEOUT_SECONDS}. Current: ${config.raven_mcp.timeout ?? 600}s` })
|
|
2341
|
+
} else {
|
|
2342
|
+
updateConfig({ raven_mcp: { timeout: seconds } })
|
|
2343
|
+
output.parts.push({ type: "text", text: `Raven MCP timeout set to ${seconds}s. Takes effect immediately.` })
|
|
2344
|
+
}
|
|
2345
|
+
} else {
|
|
2346
|
+
output.parts.push({ type: "text", text: "Usage:\n /raven mcp\n /raven mcp refresh [server_name]\n /raven mcp detail full|minimized\n /raven mcp model <provider/model>\n /raven mcp effort <value>\n /raven mcp timeout <seconds>" })
|
|
1887
2347
|
}
|
|
1888
2348
|
} else if (arg === "update") {
|
|
1889
2349
|
try {
|
|
@@ -1899,37 +2359,11 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1899
2359
|
} catch (err: any) {
|
|
1900
2360
|
output.parts.push({ type: "text", text: `Raven update check failed: ${err?.message ?? err}\n\n${manualUpdateText()}` })
|
|
1901
2361
|
}
|
|
1902
|
-
} else
|
|
1903
|
-
const
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
}
|
|
1907
|
-
updateConfig({ model })
|
|
1908
|
-
ravenProviderIssue = undefined
|
|
1909
|
-
output.parts.push({ type: "text", text: `Raven model set to: ${model}\nRestart opencode for the change to take effect.` })
|
|
1910
|
-
}
|
|
1911
|
-
} else if (arg.startsWith("effort ")) {
|
|
1912
|
-
const effort = raw.slice(7).trim()
|
|
1913
|
-
if (!effort) {
|
|
1914
|
-
output.parts.push({ type: "text", text: `Usage: /raven effort <value>\nCurrent: ${config.reasoning_effort || fm.reasoning_effort || "(default)"}` })
|
|
1915
|
-
} else {
|
|
1916
|
-
updateConfig({ reasoning_effort: effort })
|
|
1917
|
-
output.parts.push({ type: "text", text: `Raven reasoning effort set to: ${effort}\nRestart opencode for the change to take effect.` })
|
|
1918
|
-
}
|
|
1919
|
-
} else if (arg.startsWith("timeout ")) {
|
|
1920
|
-
const secs = Number(raw.slice(8).trim())
|
|
1921
|
-
if (!Number.isInteger(secs) || secs < 10 || secs > MAX_TIMEOUT_SECONDS) {
|
|
1922
|
-
output.parts.push({ type: "text", text: `Usage: /raven timeout <seconds>\nMust be between 10 and ${MAX_TIMEOUT_SECONDS}. Current: ${config.timeout ?? 600}s` })
|
|
1923
|
-
} else {
|
|
1924
|
-
updateConfig({ timeout: secs })
|
|
1925
|
-
output.parts.push({ type: "text", text: `Raven timeout set to ${secs}s. Takes effect immediately.` })
|
|
1926
|
-
}
|
|
1927
|
-
} else {
|
|
1928
|
-
const enabled = config.enabled ? "enabled" : "disabled"
|
|
1929
|
-
const model = config.model || fm.model || "(default)"
|
|
1930
|
-
const effort = config.reasoning_effort || fm.reasoning_effort || "(default)"
|
|
1931
|
-
const timeout = config.timeout ?? 600
|
|
1932
|
-
const providerIssue = ravenProviderIssue ? `\nProvider issue: ${ravenProviderIssue}` : ""
|
|
2362
|
+
} else {
|
|
2363
|
+
const seek = config.raven_seek
|
|
2364
|
+
const mcp = config.raven_mcp
|
|
2365
|
+
const seekIssue = ravenProviderIssues.seek ? `\n Provider issue: ${ravenProviderIssues.seek}` : ""
|
|
2366
|
+
const mcpIssue = ravenProviderIssues.mcp ? `\n Provider issue: ${ravenProviderIssues.mcp}` : ""
|
|
1933
2367
|
let update = "Update: unable to check npm."
|
|
1934
2368
|
try {
|
|
1935
2369
|
const info = await getUpdateInfo()
|
|
@@ -1939,23 +2373,22 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1939
2373
|
? `Update: up to date (latest ${info.latest}).`
|
|
1940
2374
|
: "Update: unable to check npm."
|
|
1941
2375
|
} catch { /* keep fallback */ }
|
|
1942
|
-
output.parts.push({ type: "text", text: `Raven
|
|
2376
|
+
output.parts.push({ type: "text", text: `Raven version: ${PACKAGE_VERSION}.\nSearch agent: ${seek.model || fm.model || "(default)"}. Reasoning: ${seek.reasoning_effort || fm.reasoning_effort || "(default)"}. Timeout: ${seek.timeout ?? 600}s.${seekIssue}\nMCP agent: ${mcp.model || mcpFm.model || "(default)"}. Reasoning: ${mcp.reasoning_effort || mcpFm.reasoning_effort || "(default)"}. Timeout: ${mcp.timeout ?? 600}s.${mcpIssue}\n${update}\n\n${routeSummary()}\n\n${mcpSummary()}\n\n${statsText()}\n\nRun /raven help for commands.` })
|
|
1943
2377
|
}
|
|
1944
2378
|
},
|
|
1945
2379
|
|
|
1946
2380
|
async "tool.execute.before"(input: any, output: any) {
|
|
1947
|
-
if (input.tool === "raven_seek"
|
|
1948
|
-
throw new Error(
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
if (input.tool === "
|
|
1952
|
-
throw new Error("
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
if (
|
|
1956
|
-
if (
|
|
1957
|
-
if (
|
|
1958
|
-
if (config.excludeTools?.some((name) => name.toLowerCase() === input.tool.toLowerCase())) return
|
|
2381
|
+
if ((input.tool === "raven_seek" || input.tool === "raven_mcp") && ravenSessions.has(input.sessionID)) {
|
|
2382
|
+
throw new Error(`${input.tool} cannot be called from inside a Raven agent. Use that agent's direct tools instead.`)
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
if (input.tool === "raven_mcp_bridge" && ravenSessionKinds.get(input.sessionID) !== "mcp" && sessionAgents.get(input.sessionID) !== "raven-mcp") {
|
|
2386
|
+
throw new Error("raven_mcp_bridge is only available inside the Raven MCP agent. Use raven_mcp for on-demand MCP requests.")
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
if (ravenSessions.has(input.sessionID)) return
|
|
2390
|
+
if (isExcluded(sessionAgents.get(input.sessionID))) return
|
|
2391
|
+
if (config.raven_seek.excludeTools?.some((name) => name.toLowerCase() === input.tool.toLowerCase())) return
|
|
1959
2392
|
|
|
1960
2393
|
if (input.tool === "task" && output.args?.subagent_type === "raven") {
|
|
1961
2394
|
ravenTaskCalls.add(input.callID)
|