opencode-raven 2.3.0 → 2.3.2
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 +95 -76
- package/README.md +25 -1
- package/background.ts +144 -0
- package/package.json +27 -20
- package/raven-core.ts +1715 -1692
- package/rpc.ts +45 -27
- package/tui.ts +105 -36
- package/v2.ts +355 -320
package/v2.ts
CHANGED
|
@@ -1,331 +1,366 @@
|
|
|
1
|
-
import type { Plugin, Agent } from "@opencode/plugin"
|
|
2
|
-
import type { PluginInput, Hooks, ToolContext as V1ToolContext } from "@opencode-ai/plugin"
|
|
3
|
-
import type { SessionMessageInfo, SessionInfo } from "@opencode/client"
|
|
4
|
-
import { z } from "zod"
|
|
1
|
+
import type { Plugin, Agent } from "@opencode/plugin"
|
|
2
|
+
import type { PluginInput, Hooks, ToolContext as V1ToolContext } from "@opencode-ai/plugin"
|
|
3
|
+
import type { SessionMessageInfo, SessionInfo } from "@opencode/client"
|
|
4
|
+
import { z } from "zod"
|
|
5
5
|
import createRavenRuntime from "./raven-core"
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
type
|
|
10
|
-
type
|
|
11
|
-
type
|
|
12
|
-
type
|
|
13
|
-
type
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (message.type === "
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
6
|
+
import { backgroundDelegations } from "./background"
|
|
7
|
+
import { RavenRpc, mcpRefreshArguments, mcpRefreshToast } from "./rpc"
|
|
8
|
+
|
|
9
|
+
type Context = Plugin.Context
|
|
10
|
+
type RequestOptions = NonNullable<Parameters<Context["session"]["get"]>[1]>
|
|
11
|
+
type Runtime = Awaited<ReturnType<typeof createRavenRuntime>>
|
|
12
|
+
type Registration = { dispose(): Promise<void> }
|
|
13
|
+
type Call = { path?: { id: string }; body?: Record<string, any>; signal?: AbortSignal }
|
|
14
|
+
type AgentDefinition = {
|
|
15
|
+
description: string
|
|
16
|
+
mode: "subagent"
|
|
17
|
+
hidden: boolean
|
|
18
|
+
disable: boolean
|
|
19
|
+
model?: string
|
|
20
|
+
options: Record<string, unknown>
|
|
21
|
+
permission: Record<string, "allow" | "deny" | "ask">
|
|
22
|
+
prompt: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function modelRef(value: string): NonNullable<Agent.Info["model"]> {
|
|
26
|
+
const slash = value.indexOf("/")
|
|
27
|
+
if (slash < 1 || slash === value.length - 1) throw new Error(`Invalid Raven model: ${value}`)
|
|
28
|
+
const [id, variant] = value.slice(slash + 1).split("#")
|
|
29
|
+
if (!id || value.slice(0, slash).includes("#") || value.split("#").length > 2 || (variant !== undefined && !variant)) throw new Error(`Invalid Raven model: ${value}`)
|
|
30
|
+
return { providerID: value.slice(0, slash), id, ...(variant ? { variant } : {}) } as NonNullable<Agent.Info["model"]>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function legacyError(error: { type: string; message: string; status?: number; response?: { body: string } }) {
|
|
34
|
+
return { name: error.type, data: { message: error.message, statusCode: error.status, responseBody: error.response?.body } }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// V2 projects messages directly, rather than wrapping them in { info, parts }.
|
|
38
|
+
// Preserve agent switches when validating persisted continuation history.
|
|
39
|
+
function legacyMessages(messages: readonly SessionMessageInfo[], session: SessionInfo) {
|
|
40
|
+
let agent = typeof session.metadata?.opencodeRavenAgent === "string"
|
|
41
|
+
? session.metadata.opencodeRavenAgent : session.agent
|
|
42
|
+
const currentAgentMatches = session.agent === agent
|
|
43
|
+
return messages.map((message) => {
|
|
44
|
+
if (message.type === "agent-switched") agent = message.agent
|
|
45
|
+
if (message.type === "assistant") {
|
|
46
|
+
return { info: { ...message, role: "assistant" }, parts: message.content }
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
info: { ...message, role: message.type, agent: currentAgentMatches ? agent : session.agent },
|
|
50
|
+
parts: message.type === "user" ? [{ type: "text", text: message.text }] : [],
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function setupRaven(ctx: Context): Promise<Plugin.Cleanup> {
|
|
56
|
+
const registrations: Registration[] = []
|
|
56
57
|
const controller = new AbortController()
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
let
|
|
62
|
-
let
|
|
63
|
-
let
|
|
64
|
-
let
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
disposed
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
58
|
+
const background = backgroundDelegations(ctx, controller.signal)
|
|
59
|
+
const location = { directory: ctx.location.directory }
|
|
60
|
+
const agents: Record<string, AgentDefinition> = {}
|
|
61
|
+
const activeSessions = new Set<string>()
|
|
62
|
+
let emitNotice: (notice: z.infer<typeof RavenRpc.events.notice.schema>) => Promise<void> = async () => {}
|
|
63
|
+
let runtime: Runtime | undefined
|
|
64
|
+
let startupTimer: ReturnType<typeof setTimeout> | undefined
|
|
65
|
+
let eventPump: Promise<void> | undefined
|
|
66
|
+
let disposed = false
|
|
67
|
+
|
|
68
|
+
const requestOptions = (call: Call): RequestOptions => ({ signal: call.signal ? AbortSignal.any([call.signal, controller.signal]) : controller.signal })
|
|
69
|
+
|
|
70
|
+
// Adapt only the host boundary. Raven's MCP transports, state, timeout logic,
|
|
71
|
+
// command parser, and continuation validation remain shared with v1.
|
|
72
|
+
const client = {
|
|
73
|
+
provider: {
|
|
74
|
+
async list(call: Call) {
|
|
75
|
+
const response = await ctx.model.list({ location }, requestOptions(call))
|
|
76
|
+
const models = response.data.filter((model) => model.providerID === "opencode" && model.enabled)
|
|
77
|
+
return { data: {
|
|
78
|
+
connected: models.length ? ["opencode"] : [],
|
|
79
|
+
all: [{ id: "opencode", models: Object.fromEntries(models.map((model) => [model.id, {
|
|
80
|
+
id: model.id,
|
|
81
|
+
status: model.status,
|
|
82
|
+
release_date: new Date(model.time.released).toISOString(),
|
|
83
|
+
tool_call: model.capabilities.tools,
|
|
84
|
+
cost: model.cost.length && model.cost.every((tier) => tier.input === 0 && tier.output === 0)
|
|
85
|
+
? { input: 0, output: 0 } : { input: 1, output: 1 },
|
|
86
|
+
limit: model.limit,
|
|
87
|
+
}])) }],
|
|
88
|
+
} }
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
session: {
|
|
92
|
+
async get(call: Call) {
|
|
93
|
+
return { data: await ctx.session.get({ sessionID: call.path!.id }, requestOptions(call)) }
|
|
94
|
+
},
|
|
95
|
+
async create(call: Call) {
|
|
96
|
+
const agent = String(call.body!.agent)
|
|
97
|
+
const definition = agents[agent]
|
|
98
|
+
if (!definition?.model) throw new Error(`Raven agent ${agent} has no configured model`)
|
|
99
|
+
return { data: await ctx.session.create({
|
|
100
|
+
parentID: call.body!.parentID,
|
|
101
|
+
title: call.body!.title,
|
|
102
|
+
agent,
|
|
103
|
+
model: modelRef(definition.model),
|
|
104
|
+
location,
|
|
105
|
+
metadata: { opencodeRavenAgent: agent },
|
|
106
|
+
}, requestOptions(call)) }
|
|
107
|
+
},
|
|
108
|
+
async update(call: Call) {
|
|
109
|
+
await ctx.session.update({ sessionID: call.path!.id, title: call.body!.title }, requestOptions(call))
|
|
110
|
+
},
|
|
111
|
+
async messages(call: Call) {
|
|
112
|
+
const sessionID = call.path!.id
|
|
113
|
+
const options = requestOptions(call)
|
|
114
|
+
const [messages, session] = await Promise.all([
|
|
115
|
+
ctx.session.context({ sessionID }, options), ctx.session.get({ sessionID }, options),
|
|
116
|
+
])
|
|
117
|
+
return { data: legacyMessages(messages, session) }
|
|
118
|
+
},
|
|
119
|
+
async abort(call: Call) {
|
|
120
|
+
await ctx.session.interrupt({ sessionID: call.path!.id, resume: false }, requestOptions(call))
|
|
121
|
+
},
|
|
122
|
+
async prompt(call: Call) {
|
|
123
|
+
const sessionID = call.path!.id
|
|
124
|
+
const options = requestOptions(call)
|
|
125
|
+
const session = await ctx.session.get({ sessionID }, options)
|
|
126
|
+
if (session.agent !== call.body!.agent) throw new Error("Raven session agent changed; start a new retrieval")
|
|
127
|
+
activeSessions.add(sessionID)
|
|
128
|
+
try {
|
|
129
|
+
const previousIDs = new Set((await ctx.session.context({ sessionID }, options)).map((message) => message.id))
|
|
130
|
+
const admitted = await ctx.session.prompt({
|
|
131
|
+
sessionID,
|
|
132
|
+
text: call.body!.parts.map((part: { text: string }) => part.text).join("\n"),
|
|
133
|
+
delivery: "queue",
|
|
134
|
+
}, options)
|
|
135
|
+
// Admission is not completion. Wait for idle, then read only this turn's
|
|
136
|
+
// final assistant message; a follow-up must never return an older answer.
|
|
137
|
+
await ctx.session.wait({ sessionID }, options)
|
|
138
|
+
options.signal?.throwIfAborted()
|
|
139
|
+
const [messages, completed] = await Promise.all([
|
|
140
|
+
ctx.session.context({ sessionID }, options), ctx.session.get({ sessionID }, options),
|
|
141
|
+
])
|
|
142
|
+
const boundary = messages.findIndex((message) => message.id === admitted.id)
|
|
143
|
+
// Compaction may remove the admitted user message from active context.
|
|
144
|
+
// In that case, only newly projected messages can belong to this turn.
|
|
145
|
+
const candidates = boundary >= 0 ? messages.slice(boundary + 1) : messages.filter((message) => !previousIDs.has(message.id))
|
|
146
|
+
const nextUser = candidates.findIndex((message) => message.type === "user")
|
|
147
|
+
const turn = nextUser < 0 ? candidates : candidates.slice(0, nextUser)
|
|
148
|
+
const failed = turn.find((message) => message.type === "assistant" && (message.error || message.retry))
|
|
149
|
+
if (failed?.type === "assistant") throw new Error(JSON.stringify(legacyError(failed.error ?? failed.retry!.error)))
|
|
150
|
+
if (completed.outcome === "failed" || completed.outcome === "interrupted") {
|
|
151
|
+
throw new Error(`Raven execution ${completed.outcome}; no completed answer is available`)
|
|
152
|
+
}
|
|
153
|
+
const answer = turn.filter((message) => message.type === "assistant").at(-1)
|
|
154
|
+
if (answer?.type !== "assistant") {
|
|
155
|
+
throw new Error(`Raven finished without an assistant answer (${completed.outcome ?? "unknown outcome"})`)
|
|
156
|
+
}
|
|
157
|
+
return { data: { parts: answer.content } }
|
|
158
|
+
} finally {
|
|
159
|
+
activeSessions.delete(sessionID)
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
tui: {
|
|
164
|
+
async showToast({ body }: { body: { title?: string; message: string; variant?: "info" | "warning"; duration?: number } }) {
|
|
165
|
+
if (disposed) return
|
|
166
|
+
await emitNotice({ kind: "toast", ...body })
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const cleanup = async () => {
|
|
172
|
+
if (disposed) return
|
|
173
|
+
disposed = true
|
|
174
|
+
const running = [...activeSessions]
|
|
175
|
+
controller.abort()
|
|
176
|
+
if (startupTimer) clearTimeout(startupTimer)
|
|
175
177
|
await Promise.allSettled(running.map((sessionID) => ctx.session.interrupt({ sessionID, resume: false }, { signal: AbortSignal.timeout(5000) })))
|
|
176
|
-
await
|
|
177
|
-
await
|
|
178
|
-
await
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
178
|
+
await background.dispose()
|
|
179
|
+
await Promise.allSettled(registrations.reverse().map((registration) => registration.dispose()))
|
|
180
|
+
await runtime?.dispose()
|
|
181
|
+
await eventPump
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
runtime = await createRavenRuntime({
|
|
186
|
+
client: client as unknown as PluginInput["client"],
|
|
187
|
+
directory: ctx.location.directory,
|
|
188
|
+
project: { id: ctx.location.project.id },
|
|
189
|
+
}, "v2")
|
|
190
|
+
const raven = runtime
|
|
191
|
+
const mcp = await ctx.mcp.list({ location }, { signal: controller.signal })
|
|
192
|
+
const config = {
|
|
193
|
+
mcp: Object.fromEntries(mcp.data.map((server) => [server.name, { enabled: server.status.status !== "disabled" }])),
|
|
194
|
+
agent: agents,
|
|
195
|
+
instructions: [] as string[],
|
|
196
|
+
}
|
|
197
|
+
await raven.config(config)
|
|
198
|
+
registrations.push(await ctx.mcp.transform((editor) => {
|
|
199
|
+
raven.globalMcpRouting(editor.list().filter(([, server]) => !server.disabled).map(([name]) => name))
|
|
200
|
+
}))
|
|
201
|
+
|
|
202
|
+
async function executeCommand(raw: string) {
|
|
203
|
+
const output = { parts: [] as Array<{ type: string; text: string }> }
|
|
204
|
+
await raven["command.execute.before"]({ command: "raven", arguments: raw, sessionID: "" }, output)
|
|
205
|
+
return { text: output.parts.map((part) => part.text).join("\n") }
|
|
206
|
+
}
|
|
207
|
+
const rpc = await ctx.rpc.register(RavenRpc, {
|
|
208
|
+
command: async ({ arguments: raw }) => executeCommand(raw),
|
|
209
|
+
refreshMcps: async ({ serverName }) => raven.refreshMcps(serverName),
|
|
210
|
+
})
|
|
211
|
+
registrations.push(rpc)
|
|
212
|
+
emitNotice = (notice) => rpc.events.emit("notice", notice)
|
|
213
|
+
|
|
214
|
+
// Delegation needs no extra confirmation. Preserve deny rules and leave
|
|
215
|
+
// child tools and the Raven-only MCP bridge's permissions unchanged.
|
|
216
|
+
registrations.push(await ctx.permission.hook("evaluate", (event) => {
|
|
217
|
+
if (event.effect === "ask" && (event.action === "raven_seek" || event.action === "raven_mcp")) {
|
|
218
|
+
event.effect = "allow"
|
|
219
|
+
}
|
|
220
|
+
}))
|
|
221
|
+
|
|
222
|
+
registrations.push(await ctx.agent.transform((editor) => {
|
|
223
|
+
for (const [id, definition] of Object.entries(agents)) {
|
|
224
|
+
if (definition.disable) { editor.remove(id); continue }
|
|
225
|
+
// The shipped registry upserts on update, including previously absent IDs.
|
|
226
|
+
editor.update(id, (agent) => {
|
|
227
|
+
agent.name = id as Agent.Name
|
|
228
|
+
agent.description = definition.description
|
|
229
|
+
agent.mode = definition.mode
|
|
230
|
+
agent.hidden = definition.hidden
|
|
231
|
+
agent.model = definition.model ? modelRef(definition.model) : undefined
|
|
232
|
+
agent.system = definition.prompt
|
|
233
|
+
agent.request.body = { ...definition.options }
|
|
234
|
+
agent.permissions = Object.entries(definition.permission).map(([action, effect]) => ({
|
|
235
|
+
action: action === "bash" ? "shell" : action === "task" ? "subagent" : action,
|
|
236
|
+
resource: "*", effect,
|
|
237
|
+
}))
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
}))
|
|
241
|
+
|
|
228
242
|
const tools: NonNullable<Hooks["tool"]> = raven.tool
|
|
229
|
-
registrations.push(await ctx.tool.transform((editor) => {
|
|
230
|
-
for (const [name, definition] of Object.entries(tools)) {
|
|
231
|
-
const
|
|
243
|
+
registrations.push(await ctx.tool.transform((editor) => {
|
|
244
|
+
for (const [name, definition] of Object.entries(tools)) {
|
|
245
|
+
const delegation = name === "raven_seek" || name === "raven_mcp"
|
|
246
|
+
const schema = z.object({ ...definition.args, ...(delegation ? {
|
|
247
|
+
background: z.boolean().optional().describe("Defaults to true. Return immediately and deliver compact findings to this calling session on completion. Set false to wait when the next step depends on these findings."),
|
|
248
|
+
} : {}) })
|
|
232
249
|
editor.add({
|
|
233
|
-
name, description: definition.description,
|
|
234
|
-
input: z.toJSONSchema(schema),
|
|
235
|
-
options: { permission: name, codemode: false },
|
|
236
|
-
async execute(input, context) {
|
|
237
|
-
await raven["chat.message"]({ sessionID: context.sessionID, agent: context.agent }, {})
|
|
238
|
-
await raven["tool.execute.before"]({ tool: name, sessionID: context.sessionID, callID: context.id }, { args: input })
|
|
239
|
-
let metadata: Record<string, unknown> = {}
|
|
240
|
-
let progress = Promise.resolve()
|
|
241
|
-
const legacyContext: V1ToolContext = {
|
|
242
|
-
sessionID: context.sessionID, messageID: context.messageID, agent: context.agent,
|
|
243
|
-
directory: ctx.location.directory, worktree: ctx.location.project.directory,
|
|
244
|
-
abort: AbortSignal.any([context.signal, controller.signal]),
|
|
245
|
-
metadata(update) {
|
|
246
|
-
metadata = { ...metadata, ...update.metadata, ...(update.title ? { title: update.title } : {}) }
|
|
247
|
-
progress = progress.then(() => context.progress(metadata)).catch(() => {})
|
|
248
|
-
},
|
|
249
|
-
async ask() { throw new Error("Raven tools do not request additional permissions") },
|
|
250
|
-
}
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}))
|
|
287
|
-
registrations.push(await ctx.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
250
|
+
name, description: definition.description + (delegation ? " Runs in the background by default on V2; completion findings arrive automatically. Use background: false to wait for dependent evidence." : ""),
|
|
251
|
+
input: z.toJSONSchema(schema),
|
|
252
|
+
options: { permission: name, codemode: false },
|
|
253
|
+
async execute(input, context) {
|
|
254
|
+
await raven["chat.message"]({ sessionID: context.sessionID, agent: context.agent }, {})
|
|
255
|
+
await raven["tool.execute.before"]({ tool: name, sessionID: context.sessionID, callID: context.id }, { args: input })
|
|
256
|
+
let metadata: Record<string, unknown> = {}
|
|
257
|
+
let progress = Promise.resolve()
|
|
258
|
+
const legacyContext: V1ToolContext = {
|
|
259
|
+
sessionID: context.sessionID, messageID: context.messageID, agent: context.agent,
|
|
260
|
+
directory: ctx.location.directory, worktree: ctx.location.project.directory,
|
|
261
|
+
abort: AbortSignal.any([context.signal, controller.signal]),
|
|
262
|
+
metadata(update) {
|
|
263
|
+
metadata = { ...metadata, ...update.metadata, ...(update.title ? { title: update.title } : {}) }
|
|
264
|
+
progress = progress.then(() => context.progress(metadata)).catch(() => {})
|
|
265
|
+
},
|
|
266
|
+
async ask() { throw new Error("Raven tools do not request additional permissions") },
|
|
267
|
+
}
|
|
268
|
+
const parsed: Record<string, any> = schema.parse(input)
|
|
269
|
+
const execute = (toolContext: V1ToolContext) => definition.execute(parsed, toolContext)
|
|
270
|
+
const result = delegation && parsed.background !== false
|
|
271
|
+
? await background.run(name, String(parsed.query), legacyContext, execute)
|
|
272
|
+
: await execute(legacyContext)
|
|
273
|
+
await progress
|
|
274
|
+
return typeof result === "string" ? { content: result } : {
|
|
275
|
+
content: result.output,
|
|
276
|
+
metadata: { ...metadata, ...result.metadata, ...(result.title ? { title: result.title } : {}) },
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
}))
|
|
282
|
+
|
|
283
|
+
registrations.push(await ctx.tool.hook("execute.before", async (event) => {
|
|
284
|
+
await raven["chat.message"]({ sessionID: event.sessionID, agent: event.agent }, {})
|
|
285
|
+
const output = { args: event.input }
|
|
286
|
+
await raven["tool.execute.before"]({
|
|
287
|
+
...event,
|
|
288
|
+
callID: event.id,
|
|
289
|
+
}, output)
|
|
290
|
+
event.input = output.args
|
|
291
|
+
}))
|
|
292
|
+
registrations.push(await ctx.tool.hook("execute.after", async (event) => {
|
|
293
|
+
if (event.status !== "completed") return
|
|
294
|
+
await raven["tool.execute.after"]({ ...event, callID: event.id }, {
|
|
295
|
+
metadata: event.result.metadata, output: event.result.content ?? "",
|
|
296
|
+
})
|
|
297
|
+
}))
|
|
298
|
+
registrations.push(await ctx.session.hook("context", async (event) => {
|
|
299
|
+
await raven["chat.message"]({ sessionID: event.sessionID, agent: event.agent }, {})
|
|
300
|
+
if (event.agent === "raven") return
|
|
301
|
+
event.system.push({ type: "text", text: event.agent === "raven-mcp" ? raven.mcpGuidance() : raven.guidance() })
|
|
302
|
+
if (event.agent !== "raven-mcp") event.system.push({ type: "text", text: "On OpenCode V2, raven_seek and raven_mcp run in the background by default. Their initial response is a session handle, not findings. Continue independent work while waiting for the automatic completion message. Do not poll or repeat pending retrievals, and do not finalize dependent conclusions without their findings. Set background: false if your next step requires the answer. Follow-ups must wait until the earlier request finishes." })
|
|
303
|
+
}))
|
|
304
|
+
registrations.push(await ctx.session.hook("retry", async (event) => {
|
|
305
|
+
await raven.event({ event: {
|
|
306
|
+
type: "session.error", properties: { sessionID: event.sessionID, error: legacyError(event.error) },
|
|
307
|
+
} })
|
|
308
|
+
}))
|
|
309
|
+
registrations.push(await ctx.command.transform((editor) => {
|
|
310
|
+
editor.add({
|
|
311
|
+
name: "raven", description: "Manage Raven routing, MCPs, models, timeouts, updates, and status",
|
|
312
|
+
async execute({ sessionID, prompt }) {
|
|
313
|
+
const refresh = mcpRefreshArguments(prompt.text)
|
|
314
|
+
if (refresh) {
|
|
315
|
+
await emitNotice({ kind: "toast", sessionID, title: "Raven MCP", message: `Refreshing ${refresh.serverName ?? "on-demand MCPs"}…`, variant: "info" })
|
|
316
|
+
try {
|
|
317
|
+
const result = await raven.refreshMcps(refresh.serverName)
|
|
318
|
+
await emitNotice({ kind: "toast", sessionID, ...mcpRefreshToast(result) })
|
|
319
|
+
} catch (error) {
|
|
320
|
+
await emitNotice({ kind: "toast", sessionID, title: "Raven MCP", message: `Refresh failed: ${String(error)}`, variant: "error" })
|
|
321
|
+
}
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
const { text } = await executeCommand(prompt.text)
|
|
325
|
+
await emitNotice({ kind: "command", sessionID, message: text, title: "Raven" })
|
|
326
|
+
},
|
|
327
|
+
})
|
|
328
|
+
}))
|
|
329
|
+
|
|
330
|
+
eventPump = (async () => {
|
|
331
|
+
try {
|
|
332
|
+
for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
|
|
333
|
+
if (event.type === "model.updated") {
|
|
334
|
+
await raven.event({ event: { type: "raven.models.ready" } })
|
|
335
|
+
} else if (event.type === "session.created") {
|
|
336
|
+
const data = event.data
|
|
337
|
+
await raven.event({ event: { type: event.type, properties: { info: { ...data, id: data.sessionID } } } })
|
|
305
338
|
} else if (event.type === "session.deleted") {
|
|
306
339
|
const data = event.data
|
|
307
|
-
await
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
340
|
+
await background.deleted(data.sessionID)
|
|
341
|
+
await raven.event({ event: { type: event.type, properties: { info: { id: data.sessionID } } } })
|
|
342
|
+
} else if (event.type === "session.execution.failed" || event.type === "session.retry.scheduled") {
|
|
343
|
+
const data = event.data
|
|
344
|
+
await raven.event({ event: { type: "session.error", properties: { sessionID: data.sessionID, error: legacyError(data.error) } } })
|
|
345
|
+
} else if (event.type === "session.text.delta" || event.type === "session.reasoning.delta" || event.type === "session.tool.input.delta" || event.type === "session.text.started" || event.type === "session.reasoning.started" || event.type === "session.tool.input.started") {
|
|
346
|
+
const data = event.data
|
|
347
|
+
await raven.event({ event: { type: "message.part.updated", properties: { part: { sessionID: data.sessionID, type: "text" } } } })
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
} catch (error) {
|
|
351
|
+
if (!controller.signal.aborted) console.error("Raven event subscription failed:", error)
|
|
352
|
+
}
|
|
319
353
|
})()
|
|
320
354
|
await ctx.agent.reload()
|
|
321
355
|
await ctx.tool.reload()
|
|
322
356
|
await ctx.command.reload()
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
357
|
+
await background.recover()
|
|
358
|
+
startupTimer = setTimeout(() => {
|
|
359
|
+
void raven.event({ event: { type: "raven.ready" } }).catch((error) => console.error("Raven startup failed:", error))
|
|
360
|
+
}, 3000)
|
|
361
|
+
return cleanup
|
|
362
|
+
} catch (error) {
|
|
363
|
+
await cleanup()
|
|
364
|
+
throw error
|
|
365
|
+
}
|
|
366
|
+
}
|