opencode-raven 2.1.6 → 2.1.8
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 +15 -0
- package/README.md +12 -3
- package/index.ts +247 -138
- package/mcp-guidance.md +6 -2
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.8
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `raven_seek` now returns a model-visible session ID and accepts it through the optional `sessionId` argument, allowing follow-up research to continue in the same Raven session.
|
|
7
|
+
- Resumed sessions are validated as Raven children of the current main session before they can be prompted again.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Context estimates for resumed sessions count only the newly handled context instead of counting earlier Raven turns again.
|
|
11
|
+
|
|
12
|
+
## 2.1.7
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Stats and `/raven` setting updates now patch the latest on-disk configuration instead of rewriting stale in-memory state, preserving externally edited MCP servers and headers.
|
|
16
|
+
- Existing malformed or partially written configuration files are preserved for repair instead of being replaced with defaults.
|
|
17
|
+
|
|
3
18
|
## 2.1.6
|
|
4
19
|
|
|
5
20
|
### Added
|
package/README.md
CHANGED
|
@@ -54,10 +54,10 @@ In this run, Raven handled the search-heavy web research in a child session and
|
|
|
54
54
|
|
|
55
55
|
1. Your agent attempts to use a routed search, fetch, shell-discovery, or MCP tool.
|
|
56
56
|
2. Raven blocks the noisy direct call and gives the agent the exact `raven_seek` retry.
|
|
57
|
-
3. The agent
|
|
57
|
+
3. The agent creates a linked Raven child session or resumes one for a related follow-up.
|
|
58
58
|
4. Raven performs the tool-heavy work and returns only compact findings to the main session.
|
|
59
59
|
|
|
60
|
-
The main model receives Raven's final answer instead of its raw tool output. Child sessions remain available in OpenCode's session/subagent view for inspection.
|
|
60
|
+
The main model receives Raven's final answer instead of its raw tool output. Each result includes a Raven session ID that the main agent can pass back for a context-aware follow-up. Child sessions remain available in OpenCode's session/subagent view for inspection.
|
|
61
61
|
|
|
62
62
|
For direct control, mention `@Raven` in any chat. This sends the request straight to the Raven agent, but it is optional and not required for automatic routing.
|
|
63
63
|
|
|
@@ -65,9 +65,18 @@ Under the hood, Raven registers two tools:
|
|
|
65
65
|
|
|
66
66
|
| Tool | Purpose |
|
|
67
67
|
|------|---------|
|
|
68
|
-
| `raven_seek` | Creates a linked Raven child session and returns its compact result. |
|
|
68
|
+
| `raven_seek` | Creates or resumes a linked Raven child session and returns its compact result. |
|
|
69
69
|
| `raven_mcp` | Raven-only bridge to MCPs configured under `onDemandMcpServers`. |
|
|
70
70
|
|
|
71
|
+
For a related follow-up, the main agent can reuse the ID returned by the earlier call:
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
raven_seek(query="Research the available options")
|
|
75
|
+
raven_seek(query="Compare the first two in more detail", sessionId="ses_...")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Raven accepts a resumed session only when it is a Raven child of the current main session. Unrelated work should omit `sessionId` and start a fresh child.
|
|
79
|
+
|
|
71
80
|
OpenCode's built-in tools already exist; Raven routes selected calls instead of replacing those tools. Global MCPs also remain globally registered, so their schemas still enter the main session even when Raven routes their calls.
|
|
72
81
|
|
|
73
82
|
On-demand MCPs are different: Raven discovers and calls them internally, so their full schemas are not registered globally. Enabled servers are probed after startup to discover tools and health, then per-session connections are opened when Raven uses them. "On-demand" describes schema exposure and normal use, not the absence of a startup health probe.
|
package/index.ts
CHANGED
|
@@ -469,28 +469,27 @@ export default (async (input: PluginInput) => {
|
|
|
469
469
|
}
|
|
470
470
|
|
|
471
471
|
function loadConfig(): RavenConfig {
|
|
472
|
-
|
|
473
|
-
|
|
472
|
+
if (existsSync(configFile)) {
|
|
473
|
+
try {
|
|
474
474
|
const raw = JSON.parse(readFileSync(configFile, "utf-8"))
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
return normalized
|
|
475
|
+
return normalizeConfig(raw)
|
|
476
|
+
} catch {
|
|
477
|
+
// Preserve malformed or partially-written config so the user can repair it.
|
|
478
|
+
return { ...DEFAULT_CONFIG }
|
|
480
479
|
}
|
|
481
|
-
}
|
|
482
|
-
// Auto-create config file with defaults on first run
|
|
483
|
-
|
|
484
|
-
return { ...DEFAULT_CONFIG }
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function
|
|
488
|
-
try {
|
|
489
|
-
mkdirSync(ravenConfigDir, { recursive: true })
|
|
490
|
-
writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n")
|
|
491
|
-
} catch { /* non-fatal: config won't persist but toggle still works in-session */ }
|
|
492
|
-
}
|
|
493
|
-
|
|
480
|
+
}
|
|
481
|
+
// Auto-create config file with defaults on first run
|
|
482
|
+
writeConfig(DEFAULT_CONFIG)
|
|
483
|
+
return { ...DEFAULT_CONFIG }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function writeConfig(config: RavenConfig) {
|
|
487
|
+
try {
|
|
488
|
+
mkdirSync(ravenConfigDir, { recursive: true })
|
|
489
|
+
writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n")
|
|
490
|
+
} catch { /* non-fatal: config won't persist but toggle still works in-session */ }
|
|
491
|
+
}
|
|
492
|
+
|
|
494
493
|
function loadMcpMetadata(): Record<string, OnDemandMcpMetadata> {
|
|
495
494
|
const defaults = cloneOnDemandMcpMetadata(DEFAULT_ON_DEMAND_MCP_METADATA)
|
|
496
495
|
try {
|
|
@@ -515,11 +514,27 @@ export default (async (input: PluginInput) => {
|
|
|
515
514
|
writeFileSync(dynamicGuidanceFile, `## Raven on-demand MCP capabilities\n\n${onDemandMcpGuidance("delegate", dynamicGuidanceMcpOrder())}\n`, "utf-8")
|
|
516
515
|
} catch { /* non-fatal: guidance still exists in tool/subagent paths */ }
|
|
517
516
|
}
|
|
518
|
-
|
|
517
|
+
|
|
519
518
|
let config = loadConfig()
|
|
519
|
+
|
|
520
|
+
function updateConfig(update: Partial<RavenConfig> | ((latest: RavenConfig) => Partial<RavenConfig>)): RavenConfig {
|
|
521
|
+
try {
|
|
522
|
+
const raw = existsSync(configFile) ? JSON.parse(readFileSync(configFile, "utf-8")) : config
|
|
523
|
+
const latest = normalizeConfig(raw)
|
|
524
|
+
const patch = typeof update === "function" ? update(latest) : update
|
|
525
|
+
config = normalizeConfig({ ...latest, ...patch })
|
|
526
|
+
writeConfig({ ...raw, ...patch })
|
|
527
|
+
} catch {
|
|
528
|
+
// Keep malformed or partially-written external edits intact instead of replacing them.
|
|
529
|
+
const patch = typeof update === "function" ? update(config) : update
|
|
530
|
+
config = normalizeConfig({ ...config, ...patch })
|
|
531
|
+
}
|
|
532
|
+
return config
|
|
533
|
+
}
|
|
520
534
|
let mcpMetadata = loadMcpMetadata()
|
|
521
|
-
const ravenSessions = new Set<string>()
|
|
522
|
-
const
|
|
535
|
+
const ravenSessions = new Set<string>()
|
|
536
|
+
const activeRavenPrompts = new Set<string>()
|
|
537
|
+
const ravenTaskCalls = new Set<string>()
|
|
523
538
|
const sessionAgents = new Map<string, string>()
|
|
524
539
|
const ravenSessionParents = new Map<string, string>()
|
|
525
540
|
const ravenErrorWaiters = new Map<string, (error: Error) => void>()
|
|
@@ -1274,16 +1289,17 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1274
1289
|
return parsed
|
|
1275
1290
|
}
|
|
1276
1291
|
|
|
1277
|
-
// ── Context saved by Raven delegation ──
|
|
1278
|
-
let sessionBytes = 0
|
|
1279
|
-
let totalBytes = config.stats?.bytes ?? 0
|
|
1280
|
-
|
|
1281
|
-
function addBytes(bytes: number) {
|
|
1282
|
-
sessionBytes += bytes
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1292
|
+
// ── Context saved by Raven delegation ──
|
|
1293
|
+
let sessionBytes = 0
|
|
1294
|
+
let totalBytes = config.stats?.bytes ?? 0
|
|
1295
|
+
|
|
1296
|
+
function addBytes(bytes: number) {
|
|
1297
|
+
sessionBytes += bytes
|
|
1298
|
+
const updated = updateConfig((latest) => ({
|
|
1299
|
+
stats: { bytes: Math.max(totalBytes, latest.stats?.bytes ?? 0) + bytes },
|
|
1300
|
+
}))
|
|
1301
|
+
totalBytes = updated.stats?.bytes ?? totalBytes + bytes
|
|
1302
|
+
}
|
|
1287
1303
|
|
|
1288
1304
|
function formatBytes(bytes: number): string {
|
|
1289
1305
|
return bytes >= 1_000_000 ? `${(bytes / 1_000_000).toFixed(1)}MB`
|
|
@@ -1331,12 +1347,10 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1331
1347
|
return { current: PACKAGE_VERSION, latest, available: !!latest && compareVersions(latest, PACKAGE_VERSION) > 0 }
|
|
1332
1348
|
}
|
|
1333
1349
|
|
|
1334
|
-
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
m?.info?.role === "assistant" && m?.info?.tokens?.output > 0
|
|
1339
|
-
)
|
|
1350
|
+
function estimateRavenSavedCandidateBytes(messages: any[]): number {
|
|
1351
|
+
const assistantMessages = messages.filter((m: any) =>
|
|
1352
|
+
m?.info?.role === "assistant" && m?.info?.tokens?.output > 0
|
|
1353
|
+
)
|
|
1340
1354
|
const first = assistantMessages[0]
|
|
1341
1355
|
const last = assistantMessages[assistantMessages.length - 1]
|
|
1342
1356
|
const t = last?.info?.tokens
|
|
@@ -1348,9 +1362,15 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1348
1362
|
const baselineTokens = firstTokens
|
|
1349
1363
|
? (firstTokens.input ?? 0) + (firstTokens.cache?.read ?? 0) + (firstTokens.cache?.write ?? 0)
|
|
1350
1364
|
: 0
|
|
1351
|
-
const savedCandidateTokens = Math.max(0, totalTokens - baselineTokens)
|
|
1352
|
-
return savedCandidateTokens * 4
|
|
1353
|
-
}
|
|
1365
|
+
const savedCandidateTokens = Math.max(0, totalTokens - baselineTokens)
|
|
1366
|
+
return savedCandidateTokens * 4
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
async function countRavenSavedCandidateBytes(sessionId: string, signal?: AbortSignal): Promise<number> {
|
|
1370
|
+
const messagesResp = await client.session.messages({ path: { id: sessionId }, signal })
|
|
1371
|
+
const messages = (messagesResp as any)?.data ?? []
|
|
1372
|
+
return estimateRavenSavedCandidateBytes(messages)
|
|
1373
|
+
}
|
|
1354
1374
|
|
|
1355
1375
|
async function getUpdateInfo(): Promise<{ current: string; latest?: string; available: boolean }> {
|
|
1356
1376
|
if (updateInfo) return updateInfo
|
|
@@ -1514,33 +1534,85 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1514
1534
|
duplicateMcpToastPending = duplicateMcpServers.length > 0
|
|
1515
1535
|
},
|
|
1516
1536
|
|
|
1517
|
-
// Register raven_seek tool — lets agents with task:false still delegate through Raven
|
|
1518
|
-
tool: {
|
|
1519
|
-
"raven_seek": tool({
|
|
1520
|
-
description: "Unified Raven delegation tool. Use this whenever a tool/MCP is blocked by Raven, or when grep, glob, WebFetch/fetch, websearch, docs lookup, GitHub search, on-demand MCP capabilities, or search-like bash would be used. Handles routed MCP requests, on-demand MCPs, local codebase search, filesystem discovery, specific URL/page reads, web/docs research, GitHub examples, and command-output/system inspection via Raven.",
|
|
1521
|
-
args: {
|
|
1522
|
-
query: tool.schema.string().describe("What Raven should do. Include the original blocked tool/MCP request and relevant args, exact URLs when replacing WebFetch, and commands/output checks when replacing grep/rg/head over command output."),
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
const
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1537
|
+
// Register raven_seek tool — lets agents with task:false still delegate through Raven
|
|
1538
|
+
tool: {
|
|
1539
|
+
"raven_seek": tool({
|
|
1540
|
+
description: "Unified Raven delegation tool. Creates a Raven child session or resumes one for follow-up research. Use this whenever a tool/MCP is blocked by Raven, or when grep, glob, WebFetch/fetch, websearch, docs lookup, GitHub search, on-demand MCP capabilities, or search-like bash would be used. Handles routed MCP requests, on-demand MCPs, local codebase search, filesystem discovery, specific URL/page reads, web/docs research, GitHub examples, and command-output/system inspection via Raven.",
|
|
1541
|
+
args: {
|
|
1542
|
+
query: tool.schema.string().describe("What Raven should do. Include the original blocked tool/MCP request and relevant args, exact URLs when replacing WebFetch, and commands/output checks when replacing grep/rg/head over command output."),
|
|
1543
|
+
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."),
|
|
1544
|
+
},
|
|
1545
|
+
async execute(args, context) {
|
|
1546
|
+
const started = Date.now()
|
|
1547
|
+
const timeout = (config.timeout ?? 600) * 1000
|
|
1548
|
+
let sessionId: string | undefined
|
|
1549
|
+
let resumed = false
|
|
1550
|
+
let previousSavedCandidate = 0
|
|
1551
|
+
try {
|
|
1552
|
+
const requestedSessionId = args.sessionId?.trim()
|
|
1553
|
+
if (args.sessionId !== undefined && !requestedSessionId) {
|
|
1554
|
+
return { title: "Raven Seek", output: "Cannot resume Raven: sessionId must not be empty." }
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
if (requestedSessionId) {
|
|
1558
|
+
const knownRavenSession = ravenSessions.has(requestedSessionId) || sessionAgents.get(requestedSessionId) === "raven"
|
|
1559
|
+
let existingSession: any
|
|
1560
|
+
try {
|
|
1561
|
+
const sessionResp = await client.session.get({ path: { id: requestedSessionId }, signal: context.abort })
|
|
1562
|
+
existingSession = (sessionResp as any)?.data ?? sessionResp
|
|
1563
|
+
} catch (err) {
|
|
1564
|
+
if (context.abort.aborted) throw err
|
|
1565
|
+
return { title: "Raven Seek", output: `Cannot resume Raven session ${requestedSessionId}: the session was not found or could not be read.` }
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
if (!existingSession?.id || existingSession.id !== requestedSessionId) {
|
|
1569
|
+
return { title: "Raven Seek", output: `Cannot resume Raven session ${requestedSessionId}: the session was not found or could not be read.` }
|
|
1570
|
+
}
|
|
1571
|
+
if (existingSession.parentID !== context.sessionID || existingSession.projectID !== input.project.id) {
|
|
1572
|
+
return { title: "Raven Seek", output: "Cannot resume Raven: sessionId is not a Raven child of this main session." }
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
let messages: any[]
|
|
1576
|
+
try {
|
|
1577
|
+
const messagesResp = await client.session.messages({ path: { id: requestedSessionId }, signal: context.abort })
|
|
1578
|
+
const messageData = (messagesResp as any)?.data
|
|
1579
|
+
if (!Array.isArray(messageData)) {
|
|
1580
|
+
return { title: "Raven Seek", output: `Cannot resume Raven session ${requestedSessionId}: its message history could not be read.` }
|
|
1581
|
+
}
|
|
1582
|
+
messages = messageData
|
|
1583
|
+
} catch (err) {
|
|
1584
|
+
if (context.abort.aborted) throw err
|
|
1585
|
+
return { title: "Raven Seek", output: `Cannot resume Raven session ${requestedSessionId}: its message history could not be read.` }
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
const userMessages = messages.filter((message: any) => message?.info?.role === "user")
|
|
1589
|
+
const persistedRavenSession = userMessages.length > 0
|
|
1590
|
+
&& userMessages.every((message: any) => message.info.agent === "raven")
|
|
1591
|
+
if (!persistedRavenSession && !(knownRavenSession && userMessages.length === 0)) {
|
|
1592
|
+
return { title: "Raven Seek", output: "Cannot resume Raven: sessionId is not a Raven child of this main session." }
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
sessionId = requestedSessionId
|
|
1596
|
+
resumed = true
|
|
1597
|
+
previousSavedCandidate = estimateRavenSavedCandidateBytes(messages)
|
|
1598
|
+
} else {
|
|
1599
|
+
const session = await client.session.create({
|
|
1600
|
+
body: {
|
|
1601
|
+
parentID: context.sessionID,
|
|
1602
|
+
title: `raven_seek: ${args.query.slice(0, 80)}`,
|
|
1603
|
+
},
|
|
1604
|
+
signal: context.abort,
|
|
1605
|
+
})
|
|
1606
|
+
|
|
1607
|
+
sessionId = (session as any)?.data?.id ?? (session as any)?.id
|
|
1608
|
+
if (!sessionId) {
|
|
1609
|
+
return { title: "Raven Seek", output: "Failed to create Raven session." }
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
ravenSessions.add(sessionId)
|
|
1614
|
+
const activeSessionId = sessionId
|
|
1615
|
+
|
|
1544
1616
|
// Preserve the child session link for TUI clients that render it.
|
|
1545
1617
|
context.metadata({ metadata: { sessionId } })
|
|
1546
1618
|
|
|
@@ -1549,15 +1621,35 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1549
1621
|
const logFile = join(tmpdir(), "raven-sessions.log")
|
|
1550
1622
|
const ts = new Date().toISOString()
|
|
1551
1623
|
writeFileSync(logFile, `${ts} ${sessionId}\n`)
|
|
1552
|
-
} catch { /* non-fatal */ }
|
|
1624
|
+
} catch { /* non-fatal */ }
|
|
1625
|
+
|
|
1626
|
+
if (activeRavenPrompts.has(activeSessionId)) {
|
|
1627
|
+
return {
|
|
1628
|
+
title: "Raven Seek",
|
|
1629
|
+
metadata: { sessionId: activeSessionId },
|
|
1630
|
+
output: `Raven session \`${activeSessionId}\` is already handling a request. Wait for it to finish before sending another follow-up.`,
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
activeRavenPrompts.add(activeSessionId)
|
|
1553
1634
|
|
|
1554
1635
|
// Send the query to Raven with timeout. Provider/API errors are also
|
|
1555
1636
|
// surfaced through message.updated events; fail as soon as one lands
|
|
1556
1637
|
// so the caller does not wait for the full timeout on quota/auth issues.
|
|
1557
1638
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
1558
1639
|
let promptCompleted = false
|
|
1640
|
+
let sessionAbort: Promise<unknown> | undefined
|
|
1559
1641
|
const promptController = new AbortController()
|
|
1560
|
-
const abortPrompt = () =>
|
|
1642
|
+
const abortPrompt = () => {
|
|
1643
|
+
if (promptController.signal.aborted) return
|
|
1644
|
+
promptController.abort(context.abort.reason)
|
|
1645
|
+
const abortController = new AbortController()
|
|
1646
|
+
const abortTimer = setTimeout(() => abortController.abort(), Math.min(timeout, 5_000))
|
|
1647
|
+
sessionAbort = client.session.abort({
|
|
1648
|
+
path: { id: activeSessionId },
|
|
1649
|
+
signal: abortController.signal,
|
|
1650
|
+
throwOnError: true,
|
|
1651
|
+
}).catch(() => undefined).finally(() => clearTimeout(abortTimer))
|
|
1652
|
+
}
|
|
1561
1653
|
if (context.abort.aborted) abortPrompt()
|
|
1562
1654
|
else context.abort.addEventListener("abort", abortPrompt, { once: true })
|
|
1563
1655
|
const providerError = waitForRavenProviderError(sessionId)
|
|
@@ -1571,12 +1663,13 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1571
1663
|
parts: [{ type: "text", text: args.query }],
|
|
1572
1664
|
},
|
|
1573
1665
|
signal: promptController.signal,
|
|
1666
|
+
throwOnError: true,
|
|
1574
1667
|
}),
|
|
1575
1668
|
providerError,
|
|
1576
1669
|
firstProgress,
|
|
1577
1670
|
new Promise<never>((_, reject) => {
|
|
1578
1671
|
timer = setTimeout(() => {
|
|
1579
|
-
|
|
1672
|
+
abortPrompt()
|
|
1580
1673
|
reject(new Error(`Raven timed out after ${timeout / 1000}s — child session retained: ${sessionId}`))
|
|
1581
1674
|
}, timeout)
|
|
1582
1675
|
}),
|
|
@@ -1584,8 +1677,9 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1584
1677
|
promptCompleted = true
|
|
1585
1678
|
if (timer) clearTimeout(timer)
|
|
1586
1679
|
clearRavenWaiters(sessionId)
|
|
1680
|
+
if ((result as any)?.error) throw new Error(providerErrorMessage((result as any).error))
|
|
1587
1681
|
|
|
1588
|
-
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
1682
|
+
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
1589
1683
|
|
|
1590
1684
|
// Extract text from the response
|
|
1591
1685
|
const parts = (result as any)?.data?.parts ?? []
|
|
@@ -1594,46 +1688,64 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1594
1688
|
.map((p: any) => p.text)
|
|
1595
1689
|
const output = textParts.join("\n") || "Raven returned no results."
|
|
1596
1690
|
|
|
1597
|
-
// Context saved = Raven output/reasoning/cache context - compact answer returned to main session.
|
|
1598
|
-
let savedCandidate = 0
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1691
|
+
// Context saved = Raven output/reasoning/cache context - compact answer returned to main session.
|
|
1692
|
+
let savedCandidate = 0
|
|
1693
|
+
const statsController = new AbortController()
|
|
1694
|
+
const abortStats = () => statsController.abort(context.abort.reason)
|
|
1695
|
+
const statsTimer = setTimeout(() => statsController.abort(), Math.min(timeout, 5_000))
|
|
1696
|
+
if (context.abort.aborted) abortStats()
|
|
1697
|
+
else context.abort.addEventListener("abort", abortStats, { once: true })
|
|
1698
|
+
try {
|
|
1699
|
+
const totalSavedCandidate = await countRavenSavedCandidateBytes(sessionId, statsController.signal)
|
|
1700
|
+
savedCandidate = Math.max(0, totalSavedCandidate - previousSavedCandidate)
|
|
1701
|
+
} catch { /* best-effort */ }
|
|
1702
|
+
finally {
|
|
1703
|
+
clearTimeout(statsTimer)
|
|
1704
|
+
context.abort.removeEventListener("abort", abortStats)
|
|
1705
|
+
}
|
|
1602
1706
|
if (savedCandidate <= 0) {
|
|
1603
1707
|
for (const part of parts) {
|
|
1604
1708
|
if (part.args) savedCandidate += JSON.stringify(part.args).length
|
|
1605
1709
|
if (part.content) savedCandidate += typeof part.content === "string" ? part.content.length : JSON.stringify(part.content).length
|
|
1606
1710
|
}
|
|
1607
1711
|
}
|
|
1608
|
-
const
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1712
|
+
const continuation = `Raven session: \`${sessionId}\`${resumed ? " (continued)" : ""}. Pass this as \`sessionId\` in a later \`raven_seek\` call to continue the same research.`
|
|
1713
|
+
const resultOutput = `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(savedCandidate)} handled, ${formatTokens(savedCandidate)} tokens*\n\n${continuation}`
|
|
1714
|
+
addBytes(Math.max(0, savedCandidate - resultOutput.length))
|
|
1715
|
+
ravenProviderIssue = undefined
|
|
1716
|
+
|
|
1717
|
+
return { title: "Raven Seek", metadata: { sessionId }, output: resultOutput }
|
|
1613
1718
|
} finally {
|
|
1614
1719
|
context.abort.removeEventListener("abort", abortPrompt)
|
|
1615
|
-
if (!promptCompleted)
|
|
1720
|
+
if (!promptCompleted) abortPrompt()
|
|
1721
|
+
if (sessionAbort) await sessionAbort
|
|
1616
1722
|
if (timer) clearTimeout(timer)
|
|
1617
1723
|
clearRavenWaiters(sessionId)
|
|
1724
|
+
activeRavenPrompts.delete(activeSessionId)
|
|
1618
1725
|
}
|
|
1619
|
-
} catch (err: any) {
|
|
1620
|
-
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
1726
|
+
} catch (err: any) {
|
|
1727
|
+
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
1621
1728
|
const msg = String(err?.message ?? err ?? "").toLowerCase()
|
|
1729
|
+
const missingSession = /session.*not found|not found.*session/i.test(msg)
|
|
1622
1730
|
const hint =
|
|
1623
1731
|
context.abort.aborted ? "Raven search cancelled."
|
|
1624
1732
|
: /rate.?limit|too many requests|429/i.test(msg) ? "Raven rate limited — wait 30s then retry with a narrower query."
|
|
1625
|
-
: /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."
|
|
1626
|
-
: /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."
|
|
1627
|
-
: /token|context.?length|too large|too long/i.test(msg) ? "Raven query too large — shorten your query and retry."
|
|
1628
|
-
:
|
|
1629
|
-
: /
|
|
1630
|
-
: /
|
|
1631
|
-
:
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1733
|
+
: /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."
|
|
1734
|
+
: /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."
|
|
1735
|
+
: /token|context.?length|too large|too long/i.test(msg) ? "Raven query too large — shorten your query and retry."
|
|
1736
|
+
: missingSession ? "The Raven session is no longer available — retry without sessionId to start a new Raven search."
|
|
1737
|
+
: /model|unavailable|down|not found/i.test(msg) ? "Raven model unavailable — retry later, or proceed without search."
|
|
1738
|
+
: /did not produce an initial response|retry dialog/i.test(msg) ? err.message
|
|
1739
|
+
: /timeout|timed.?out|session kept/i.test(msg) ? err.message
|
|
1740
|
+
: `Raven search failed. Proceed without search — note gaps for the user. [${err.message || err}]`
|
|
1741
|
+
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)) {
|
|
1742
|
+
ravenProviderIssue = hint
|
|
1743
|
+
}
|
|
1744
|
+
const recovery = ravenProviderIssue ? `\n\n${ravenRecoveryInstructions()}` : ""
|
|
1745
|
+
const continuation = sessionId && !missingSession
|
|
1746
|
+
? `\n\nRaven session: \`${sessionId}\`. Pass this as \`sessionId\` in a later \`raven_seek\` call to retry or continue.`
|
|
1747
|
+
: ""
|
|
1748
|
+
return { title: "Raven Seek", metadata: sessionId ? { sessionId } : undefined, output: `${hint}${recovery}\n\n*Attempt took ${elapsed}s*${continuation}` }
|
|
1637
1749
|
}
|
|
1638
1750
|
},
|
|
1639
1751
|
}),
|
|
@@ -1753,6 +1865,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1753
1865
|
const sessionId = evt.properties?.info?.id
|
|
1754
1866
|
if (sessionId) {
|
|
1755
1867
|
ravenSessions.delete(sessionId)
|
|
1868
|
+
activeRavenPrompts.delete(sessionId)
|
|
1756
1869
|
sessionAgents.delete(sessionId)
|
|
1757
1870
|
clearRavenWaiters(sessionId)
|
|
1758
1871
|
for (const [parentId, childId] of ravenSessionParents) {
|
|
@@ -1814,16 +1927,14 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1814
1927
|
const raw = input.arguments.trim()
|
|
1815
1928
|
const arg = raw.toLowerCase()
|
|
1816
1929
|
|
|
1817
|
-
if (arg === "on") {
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
output.parts.push({ type: "text", text: "Raven tool/MCP routing enabled. Non-Raven agents will be redirected to raven_seek for configured tools." })
|
|
1930
|
+
if (arg === "on") {
|
|
1931
|
+
updateConfig({ enabled: true })
|
|
1932
|
+
output.parts.push({ type: "text", text: "Raven tool/MCP routing enabled. Non-Raven agents will be redirected to raven_seek for configured tools." })
|
|
1821
1933
|
} else if (arg === "help") {
|
|
1822
1934
|
output.parts.push({ type: "text", text: helpText() })
|
|
1823
|
-
} else if (arg === "off") {
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
output.parts.push({ type: "text", text: "Raven tool/MCP routing disabled. All agents can use tools directly." })
|
|
1935
|
+
} else if (arg === "off") {
|
|
1936
|
+
updateConfig({ enabled: false })
|
|
1937
|
+
output.parts.push({ type: "text", text: "Raven tool/MCP routing disabled. All agents can use tools directly." })
|
|
1827
1938
|
} else if (arg === "stats") {
|
|
1828
1939
|
output.parts.push({ type: "text", text: statsText() })
|
|
1829
1940
|
} else if (arg === "route") {
|
|
@@ -1835,16 +1946,18 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1835
1946
|
const name = parts.slice(3).join(" ").trim()
|
|
1836
1947
|
const key = kind === "tool" ? "routeTools" : kind === "keyword" ? "routeToolKeywords" : undefined
|
|
1837
1948
|
|
|
1838
|
-
if (!key || !["add", "remove", "rm"].includes(action) || !name) {
|
|
1839
|
-
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." })
|
|
1840
|
-
} else {
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1949
|
+
if (!key || !["add", "remove", "rm"].includes(action) || !name) {
|
|
1950
|
+
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." })
|
|
1951
|
+
} else {
|
|
1952
|
+
updateConfig((latest) => {
|
|
1953
|
+
const values = uniqueStrings(latest[key])
|
|
1954
|
+
const exists = values.some((value) => value.toLowerCase() === name.toLowerCase())
|
|
1955
|
+
const next = action === "add"
|
|
1956
|
+
? exists ? values : [...values, name]
|
|
1957
|
+
: values.filter((value) => value.toLowerCase() !== name.toLowerCase())
|
|
1958
|
+
return key === "routeTools" ? { routeTools: next } : { routeToolKeywords: next }
|
|
1959
|
+
})
|
|
1960
|
+
output.parts.push({ type: "text", text: routeSummary() })
|
|
1848
1961
|
}
|
|
1849
1962
|
} else if (arg === "mcp") {
|
|
1850
1963
|
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` })
|
|
@@ -1861,12 +1974,11 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1861
1974
|
})
|
|
1862
1975
|
} else if (action === "detail") {
|
|
1863
1976
|
const detail = parts[2]?.toLowerCase()
|
|
1864
|
-
if (detail !== "full" && detail !== "minimized") {
|
|
1865
|
-
output.parts.push({ type: "text", text: `Usage: /raven mcp detail full|minimized\nCurrent: ${config.onDemandMcpDescriptionDetail ?? "full"}` })
|
|
1866
|
-
} else {
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
output.parts.push({ type: "text", text: `On-demand MCP guidance detail set to: ${detail}\nRestart opencode for startup instructions/tool descriptions to fully refresh.` })
|
|
1977
|
+
if (detail !== "full" && detail !== "minimized") {
|
|
1978
|
+
output.parts.push({ type: "text", text: `Usage: /raven mcp detail full|minimized\nCurrent: ${config.onDemandMcpDescriptionDetail ?? "full"}` })
|
|
1979
|
+
} else {
|
|
1980
|
+
updateConfig({ onDemandMcpDescriptionDetail: detail })
|
|
1981
|
+
output.parts.push({ type: "text", text: `On-demand MCP guidance detail set to: ${detail}\nRestart opencode for startup instructions/tool descriptions to fully refresh.` })
|
|
1870
1982
|
}
|
|
1871
1983
|
} else {
|
|
1872
1984
|
output.parts.push({ type: "text", text: "Usage:\n /raven mcp\n /raven mcp refresh [server_name]\n /raven mcp detail full\n /raven mcp detail minimized" })
|
|
@@ -1887,31 +1999,28 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1887
1999
|
}
|
|
1888
2000
|
} else if (arg.startsWith("model ")) {
|
|
1889
2001
|
const model = raw.slice(6).trim()
|
|
1890
|
-
if (!model) {
|
|
1891
|
-
output.parts.push({ type: "text", text: `Usage: /raven model <name>\nCurrent model: ${config.model || fm.model || "(default)"}` })
|
|
1892
|
-
} else {
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
ravenProviderIssue = undefined
|
|
2002
|
+
if (!model) {
|
|
2003
|
+
output.parts.push({ type: "text", text: `Usage: /raven model <name>\nCurrent model: ${config.model || fm.model || "(default)"}` })
|
|
2004
|
+
} else {
|
|
2005
|
+
updateConfig({ model })
|
|
2006
|
+
ravenProviderIssue = undefined
|
|
1896
2007
|
output.parts.push({ type: "text", text: `Raven model set to: ${model}\nRestart opencode for the change to take effect.` })
|
|
1897
2008
|
}
|
|
1898
2009
|
} else if (arg.startsWith("effort ")) {
|
|
1899
2010
|
const effort = raw.slice(7).trim()
|
|
1900
|
-
if (!effort) {
|
|
1901
|
-
output.parts.push({ type: "text", text: `Usage: /raven effort <value>\nCurrent: ${config.reasoning_effort || fm.reasoning_effort || "(default)"}` })
|
|
1902
|
-
} else {
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
output.parts.push({ type: "text", text: `Raven reasoning effort set to: ${effort}\nRestart opencode for the change to take effect.` })
|
|
2011
|
+
if (!effort) {
|
|
2012
|
+
output.parts.push({ type: "text", text: `Usage: /raven effort <value>\nCurrent: ${config.reasoning_effort || fm.reasoning_effort || "(default)"}` })
|
|
2013
|
+
} else {
|
|
2014
|
+
updateConfig({ reasoning_effort: effort })
|
|
2015
|
+
output.parts.push({ type: "text", text: `Raven reasoning effort set to: ${effort}\nRestart opencode for the change to take effect.` })
|
|
1906
2016
|
}
|
|
1907
2017
|
} else if (arg.startsWith("timeout ")) {
|
|
1908
2018
|
const secs = Number(raw.slice(8).trim())
|
|
1909
2019
|
if (!Number.isInteger(secs) || secs < 10 || secs > MAX_TIMEOUT_SECONDS) {
|
|
1910
2020
|
output.parts.push({ type: "text", text: `Usage: /raven timeout <seconds>\nMust be between 10 and ${MAX_TIMEOUT_SECONDS}. Current: ${config.timeout ?? 600}s` })
|
|
1911
|
-
} else {
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
output.parts.push({ type: "text", text: `Raven timeout set to ${secs}s. Takes effect immediately.` })
|
|
2021
|
+
} else {
|
|
2022
|
+
updateConfig({ timeout: secs })
|
|
2023
|
+
output.parts.push({ type: "text", text: `Raven timeout set to ${secs}s. Takes effect immediately.` })
|
|
1915
2024
|
}
|
|
1916
2025
|
} else {
|
|
1917
2026
|
const enabled = config.enabled ? "enabled" : "disabled"
|
package/mcp-guidance.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Do not call tools or MCPs that Raven blocks directly. Raven may route search/fetch tools, docs/web/GitHub MCPs, user-configured tool/MCP prefixes, or on-demand MCP capabilities through `raven_seek` to save context.
|
|
4
4
|
|
|
5
|
-
Use `raven_seek(query="...")` as the next tool call for blocked tool/MCP requests. Include the original tool/MCP name, intent, and relevant arguments.
|
|
5
|
+
Use `raven_seek(query="...")` as the next tool call for blocked tool/MCP requests. Include the original tool/MCP name, intent, and relevant arguments.
|
|
6
|
+
|
|
7
|
+
When a request follows up on an earlier Raven result, pass the Raven session ID returned by that call as `sessionId` so Raven retains its research context. Omit `sessionId` for unrelated work.
|
|
6
8
|
|
|
7
9
|
For on-demand MCP requests, include the target MCP server and likely tool name in the `raven_seek` query when the listed capabilities make that obvious. Do not call `raven_mcp` directly unless you are Raven.
|
|
8
10
|
|
|
@@ -26,6 +28,8 @@ Examples:
|
|
|
26
28
|
|
|
27
29
|
`raven_seek(query="Check whether archivemount/libarchive supports ISO or UDF. Use docs, web, or command output as needed.")`
|
|
28
30
|
|
|
29
|
-
`raven_seek(query="Use the Linear MCP to find open bugs assigned to me and summarize the top 3")`
|
|
31
|
+
`raven_seek(query="Use the Linear MCP to find open bugs assigned to me and summarize the top 3")`
|
|
32
|
+
|
|
33
|
+
`raven_seek(query="Compare the first two bugs in more detail", sessionId="ses_...")`
|
|
30
34
|
|
|
31
35
|
Simple piped output filters like `command | grep ...`, `command | rg ...`, `command | findstr ...`, or `command | head ...` are allowed when they only filter bounded output from the immediately preceding command.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-raven",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.8",
|
|
4
4
|
"description": "Context-saving search and on-demand MCP routing for OpenCode through a focused Raven subagent.",
|
|
5
5
|
"main": "./index.ts",
|
|
6
6
|
"exports": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"LICENSE"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
+
"test": "bun test",
|
|
21
22
|
"typecheck": "tsc --noEmit"
|
|
22
23
|
},
|
|
23
24
|
"keywords": [
|