@shawnstack/quickforge 1.7.5 → 1.7.7
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/dist/assets/AgentProfilesPage-BfA_sQ3A.js +1 -0
- package/dist/assets/ChatPanelHost-Dul1ahKj.js +307 -0
- package/dist/assets/CloudAccountSettingsPage-qZh0lTUN.js +1 -0
- package/dist/assets/PluginsPage-CPE5tbbf.js +1 -0
- package/dist/assets/ScheduledTasksPage-PTxL_PPg.js +2 -0
- package/dist/assets/SettingsWorkspacePage-BE2T4d3P.js +1696 -0
- package/dist/assets/ShareLinksSettingsPage-Bl8fP62X.js +1 -0
- package/dist/assets/SharedConversationPage-BK9XJnEe.js +1 -0
- package/dist/assets/TerminalDock-CL7HU9DI.js +2 -0
- package/dist/assets/{WorkspaceInspector-D9kdnLDd.js → WorkspaceInspector-MNBLIKMn.js} +5 -5
- package/dist/assets/{dist-DLKC_odd.js → dist-yDHbi5ux.js} +1 -1
- package/dist/assets/{esm-Df4edDpj.js → esm-sdsK6gaB.js} +2 -2
- package/dist/assets/{icons-CXPP1fFT.js → icons-2D18YbSI.js} +1 -1
- package/dist/assets/index-BTPzyq7d.js +65 -0
- package/dist/assets/index-BiCTgUlY.css +3 -0
- package/dist/assets/{mcp-servers-dialog-BUSPvaqI.js → mcp-servers-dialog-GY7sdfIr.js} +2 -2
- package/dist/assets/{logger-8REuLPGK.js → model-display-label-C6-0eDxF.js} +1 -1
- package/dist/assets/{monaco-D6OhXgjg.js → monaco-DvXErxp2.js} +1 -1
- package/dist/assets/{react-vendor-1KC5PyRx.js → react-vendor-3UaA-C9W.js} +1 -1
- package/dist/assets/{skills-dialog-BThnIUBs.js → skills-dialog-C1fJ1Jl7.js} +3 -3
- package/dist/assets/{web-DoV8MSMn.js → web-B5qgZJRv.js} +1 -1
- package/dist/index.html +8 -8
- package/dist/sw.js +17 -0
- package/package.json +2 -1
- package/runtime-assets/agent/darwin-arm64/qf-agent +0 -0
- package/runtime-assets/agent/darwin-x64/qf-agent +0 -0
- package/runtime-assets/agent/linux-arm64/qf-agent +0 -0
- package/runtime-assets/agent/linux-x64/qf-agent +0 -0
- package/runtime-assets/agent/win32-x64/qf-agent.exe +0 -0
- package/server/access-policy.mjs +11 -0
- package/server/agent-manager.mjs +349 -48
- package/server/agent-profiles.mjs +27 -8
- package/server/auto-compaction.mjs +78 -9
- package/server/cloud/client.mjs +4 -7
- package/server/cloud/config.mjs +9 -16
- package/server/cloud/credential-store.mjs +10 -3
- package/server/cloud/identity.mjs +17 -60
- package/server/cloud/models.mjs +1 -1
- package/server/cloud/qf-agent-process.mjs +571 -0
- package/server/cloud/runtime.mjs +11 -5
- package/server/cloud/service-config.mjs +8 -4
- package/server/context-usage.mjs +11 -4
- package/server/index.mjs +96 -37
- package/server/mcp/registry.mjs +8 -7
- package/server/message-converters.mjs +40 -19
- package/server/model-catalog.mjs +11 -4
- package/server/opencode-acp-agent.mjs +1151 -0
- package/server/public-api.mjs +11 -5
- package/server/routes/agent-profiles.mjs +6 -3
- package/server/routes/agent.mjs +43 -3
- package/server/routes/backup.mjs +5 -4
- package/server/routes/cloud.mjs +54 -35
- package/server/routes/shares.mjs +7 -8
- package/server/routes/storage.mjs +9 -24
- package/server/routes/system.mjs +7 -4
- package/server/utils/process-tree.mjs +78 -0
- package/dist/assets/AgentProfilesPage-XNjuqJs7.js +0 -1
- package/dist/assets/ChatPanelHost-CuF-PZpc.js +0 -292
- package/dist/assets/CloudAccountSettingsPage-GJSoi6sO.js +0 -1
- package/dist/assets/PluginsPage-BQgp0qN-.js +0 -1
- package/dist/assets/ScheduledTasksPage-DdekATzq.js +0 -2
- package/dist/assets/SettingsWorkspacePage-Cb54Ic91.js +0 -1639
- package/dist/assets/ShareLinksSettingsPage-C75WJpaF.js +0 -1
- package/dist/assets/SharedConversationPage-CjMtSQTW.js +0 -1
- package/dist/assets/TerminalDock-D4zJBJc4.js +0 -2
- package/dist/assets/index-_ZhK6bvp.js +0 -65
- package/dist/assets/index-iVV2bP_f.css +0 -3
|
@@ -0,0 +1,1151 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { promises as fs } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { Readable, Writable } from 'node:stream'
|
|
5
|
+
import { client, methods, ndJsonStream, PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
|
|
6
|
+
import { terminateProcessTree } from './utils/process-tree.mjs'
|
|
7
|
+
|
|
8
|
+
const OPEN_CODE_COMMAND = process.platform === 'win32' ? 'opencode.cmd' : 'opencode'
|
|
9
|
+
const ACP_CLIENT_NAME = 'QuickForge'
|
|
10
|
+
const DIAGNOSTIC_LIMIT = 2048
|
|
11
|
+
const TOOL_OUTPUT_LIMIT = 16 * 1024
|
|
12
|
+
const TOOL_METADATA_LOCATION_LIMIT = 20
|
|
13
|
+
const SETUP_METADATA_BUFFER_LIMIT = 100
|
|
14
|
+
const ACP_TOOL_KIND_NAMES = Object.freeze({
|
|
15
|
+
read: 'read_file',
|
|
16
|
+
edit: 'edit_file',
|
|
17
|
+
search: 'grep_files',
|
|
18
|
+
execute: 'run_command',
|
|
19
|
+
})
|
|
20
|
+
const OPENCODE_RENDERER_TOOL_NAMES = new Set([
|
|
21
|
+
'read_file',
|
|
22
|
+
'edit_file',
|
|
23
|
+
'grep_files',
|
|
24
|
+
'run_command',
|
|
25
|
+
'opencode_tool',
|
|
26
|
+
])
|
|
27
|
+
const ACP_METADATA_UPDATE_TYPES = new Set([
|
|
28
|
+
'available_commands_update',
|
|
29
|
+
'current_mode_update',
|
|
30
|
+
'config_option_update',
|
|
31
|
+
'session_info_update',
|
|
32
|
+
'usage_update',
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
export const DEFAULT_OPENCODE_ACP_TIMEOUTS = Object.freeze({
|
|
36
|
+
initialize: 15_000,
|
|
37
|
+
sessionSetup: 30_000,
|
|
38
|
+
prompt: 60 * 60_000,
|
|
39
|
+
close: 2_000,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
function pathEntries() {
|
|
43
|
+
return String(process.env.PATH || '').split(path.delimiter).filter(Boolean)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function resolveOpenCodeCommand() {
|
|
47
|
+
for (const directory of pathEntries()) {
|
|
48
|
+
const candidate = path.join(directory, OPEN_CODE_COMMAND)
|
|
49
|
+
try {
|
|
50
|
+
const stat = await fs.stat(candidate)
|
|
51
|
+
if (stat.isFile()) return candidate
|
|
52
|
+
} catch {
|
|
53
|
+
// Continue searching PATH.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
throw Object.assign(new Error('OpenCode is unavailable. Install OpenCode and ensure the opencode command is on PATH.'), {
|
|
57
|
+
statusCode: 503,
|
|
58
|
+
errorCode: 'OPENCODE_UNAVAILABLE',
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function quoteWindowsCommandArg(value) {
|
|
63
|
+
const text = String(value).replace(/%/g, '%%')
|
|
64
|
+
if (/^[A-Za-z0-9_./:\\-]+$/.test(text)) return text
|
|
65
|
+
return `"${text.replace(/"/g, '\\"')}"`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function spawnOpenCode(command, cwd, spawnImpl = spawn, platform = process.platform) {
|
|
69
|
+
const args = ['acp', '--pure', '--cwd', cwd]
|
|
70
|
+
if (platform !== 'win32' || !/\.(?:cmd|bat)$/i.test(command)) {
|
|
71
|
+
return spawnImpl(command, args, {
|
|
72
|
+
cwd,
|
|
73
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
74
|
+
windowsHide: true,
|
|
75
|
+
...(platform === 'win32' ? {} : { detached: true }),
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const commandLine = [command, ...args].map(quoteWindowsCommandArg).join(' ')
|
|
80
|
+
return spawnImpl(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', commandLine], {
|
|
81
|
+
cwd,
|
|
82
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
83
|
+
windowsHide: true,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function messageText(message) {
|
|
88
|
+
if (typeof message === 'string') return message
|
|
89
|
+
const content = message?.content
|
|
90
|
+
if (typeof content === 'string') return content
|
|
91
|
+
if (!Array.isArray(content)) return ''
|
|
92
|
+
return content.map((part) => typeof part === 'string' ? part : part?.type === 'text' ? part.text || '' : '').filter(Boolean).join('\n')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function attachmentError(message, errorCode = 'OPENCODE_ATTACHMENT_INVALID') {
|
|
96
|
+
return Object.assign(new Error(message), { statusCode: 400, errorCode })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isBase64(value) {
|
|
100
|
+
return typeof value === 'string'
|
|
101
|
+
&& value.length % 4 === 0
|
|
102
|
+
&& /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function promptCapabilities(agentCapabilities) {
|
|
106
|
+
const capabilities = agentCapabilities?.promptCapabilities
|
|
107
|
+
return capabilities && typeof capabilities === 'object' && !Array.isArray(capabilities) ? capabilities : {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function attachmentPrompt(message, agentCapabilities) {
|
|
111
|
+
const text = messageText(message)
|
|
112
|
+
if (!message || typeof message === 'string') return [{ type: 'text', text }]
|
|
113
|
+
const attachments = message.attachments
|
|
114
|
+
const hasAttachmentRole = message.role === 'user-with-attachments'
|
|
115
|
+
const hasNonTextContent = Array.isArray(message.content) && message.content.some((part) => part && typeof part === 'object' && part.type !== 'text')
|
|
116
|
+
if (hasNonTextContent) throw attachmentError('OpenCode attachments must use the QuickForge attachments field.')
|
|
117
|
+
if (!hasAttachmentRole && attachments === undefined) return [{ type: 'text', text }]
|
|
118
|
+
if (!Array.isArray(attachments)) throw attachmentError('OpenCode attachments must be an array.')
|
|
119
|
+
|
|
120
|
+
const capabilities = promptCapabilities(agentCapabilities)
|
|
121
|
+
const prompt = text.trim() ? [{ type: 'text', text }] : []
|
|
122
|
+
attachments.forEach((attachment, index) => {
|
|
123
|
+
if (!attachment || typeof attachment !== 'object' || Array.isArray(attachment)) {
|
|
124
|
+
throw attachmentError(`OpenCode attachment ${index + 1} is invalid.`)
|
|
125
|
+
}
|
|
126
|
+
if (attachment.type !== 'image' && attachment.type !== 'document') {
|
|
127
|
+
throw attachmentError(`OpenCode attachment ${index + 1} has an unsupported type.`)
|
|
128
|
+
}
|
|
129
|
+
if (typeof attachment.id !== 'string' || !attachment.id
|
|
130
|
+
|| typeof attachment.fileName !== 'string' || !attachment.fileName
|
|
131
|
+
|| typeof attachment.mimeType !== 'string' || !attachment.mimeType
|
|
132
|
+
|| !Number.isFinite(attachment.size) || attachment.size < 0
|
|
133
|
+
|| !isBase64(attachment.content)
|
|
134
|
+
|| (attachment.extractedText !== undefined && typeof attachment.extractedText !== 'string')) {
|
|
135
|
+
throw attachmentError(`OpenCode attachment ${index + 1} has invalid fields.`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (attachment.type === 'image') {
|
|
139
|
+
if (!attachment.mimeType.startsWith('image/')) throw attachmentError(`OpenCode attachment ${index + 1} has an invalid image MIME type.`)
|
|
140
|
+
if (capabilities.image !== true) {
|
|
141
|
+
throw attachmentError('OpenCode does not advertise ACP image prompt support.', 'OPENCODE_IMAGE_UNSUPPORTED')
|
|
142
|
+
}
|
|
143
|
+
prompt.push({ type: 'image', data: attachment.content, mimeType: attachment.mimeType })
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (capabilities.embeddedContext !== true) {
|
|
148
|
+
throw attachmentError('OpenCode does not advertise ACP embedded resource prompt support.', 'OPENCODE_EMBEDDED_CONTEXT_UNSUPPORTED')
|
|
149
|
+
}
|
|
150
|
+
const resource = {
|
|
151
|
+
uri: `quickforge-attachment://prompt/${index + 1}`,
|
|
152
|
+
mimeType: attachment.mimeType,
|
|
153
|
+
...(attachment.extractedText !== undefined
|
|
154
|
+
? { text: attachment.extractedText }
|
|
155
|
+
: { blob: attachment.content }),
|
|
156
|
+
}
|
|
157
|
+
prompt.push({ type: 'resource', resource })
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
if (prompt.length === 0) throw attachmentError('OpenCode prompt must contain text or at least one attachment.')
|
|
161
|
+
return prompt
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function stripMeta(value) {
|
|
165
|
+
if (Array.isArray(value)) return value.map(stripMeta)
|
|
166
|
+
if (!value || typeof value !== 'object') return value
|
|
167
|
+
const result = {}
|
|
168
|
+
for (const [key, item] of Object.entries(value)) {
|
|
169
|
+
if (key !== '_meta') result[key] = stripMeta(item)
|
|
170
|
+
}
|
|
171
|
+
return result
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function sanitizeToolLocations(locations) {
|
|
175
|
+
if (!Array.isArray(locations)) return []
|
|
176
|
+
return locations.slice(0, TOOL_METADATA_LOCATION_LIMIT).map((location) => {
|
|
177
|
+
if (!location || typeof location.path !== 'string' || !location.path) return null
|
|
178
|
+
return {
|
|
179
|
+
path: location.path,
|
|
180
|
+
...(Number.isInteger(location.line) && location.line >= 0 ? { line: location.line } : {}),
|
|
181
|
+
}
|
|
182
|
+
}).filter(Boolean)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function toolMetadata(update, fallback = {}) {
|
|
186
|
+
const title = typeof update?.title === 'string' && update.title ? update.title : fallback.title
|
|
187
|
+
const kind = typeof update?.kind === 'string' && update.kind ? update.kind : fallback.kind
|
|
188
|
+
const locations = sanitizeToolLocations(update?.locations)
|
|
189
|
+
return {
|
|
190
|
+
...(title ? { title } : {}),
|
|
191
|
+
...(kind ? { kind } : {}),
|
|
192
|
+
...(locations.length > 0 ? { locations } : fallback.locations?.length ? { locations: fallback.locations } : {}),
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function toolName(update) {
|
|
197
|
+
return ACP_TOOL_KIND_NAMES[update?.kind] || 'opencode_tool'
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function normalizedToolInput(update, metadata) {
|
|
201
|
+
const raw = stripMeta(update?.rawInput ?? {})
|
|
202
|
+
const args = raw && typeof raw === 'object' && !Array.isArray(raw) ? { ...raw } : { rawInput: raw }
|
|
203
|
+
if (args.path === undefined && typeof args.file_path === 'string') args.path = args.file_path
|
|
204
|
+
if (args.path === undefined && typeof metadata.locations?.[0]?.path === 'string') args.path = metadata.locations[0].path
|
|
205
|
+
if (args.command === undefined && typeof args.cmd === 'string') args.command = args.cmd
|
|
206
|
+
if (args.query === undefined && typeof args.pattern === 'string') args.query = args.pattern
|
|
207
|
+
if (args.regex === undefined && typeof args.useRegex === 'boolean') args.regex = args.useRegex
|
|
208
|
+
args.__quickforgeAcp = metadata
|
|
209
|
+
return args
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function historyToolMetadata(value, fallbackName, legacyName) {
|
|
213
|
+
const metadata = toolMetadata(value)
|
|
214
|
+
if (!metadata.title && !metadata.kind && fallbackName) metadata.kind = fallbackName
|
|
215
|
+
else if (legacyName && metadata.title !== fallbackName && metadata.kind !== fallbackName) metadata.kind = fallbackName
|
|
216
|
+
return metadata
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function normalizeHistoryToolCall(block) {
|
|
220
|
+
const originalName = typeof block.name === 'string' && block.name ? block.name : 'Tool Call'
|
|
221
|
+
const rendererName = OPENCODE_RENDERER_TOOL_NAMES.has(originalName)
|
|
222
|
+
const name = rendererName ? originalName : ACP_TOOL_KIND_NAMES[originalName] || 'opencode_tool'
|
|
223
|
+
const rawArguments = stripMeta(block.arguments)
|
|
224
|
+
const args = rawArguments && typeof rawArguments === 'object' && !Array.isArray(rawArguments)
|
|
225
|
+
? { ...rawArguments }
|
|
226
|
+
: { rawInput: rawArguments }
|
|
227
|
+
const metadata = historyToolMetadata(args.__quickforgeAcp, originalName, !rendererName)
|
|
228
|
+
args.__quickforgeAcp = metadata
|
|
229
|
+
return {
|
|
230
|
+
block: { ...block, name, arguments: args },
|
|
231
|
+
name,
|
|
232
|
+
metadata,
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function normalizeOpenCodeHistoryMessages(messages) {
|
|
237
|
+
if (!Array.isArray(messages)) return []
|
|
238
|
+
const toolCalls = new Map()
|
|
239
|
+
return messages.map((message) => {
|
|
240
|
+
if (message?.role === 'assistant' && message.api === 'acp' && message.provider === 'opencode' && Array.isArray(message.content)) {
|
|
241
|
+
let changed = false
|
|
242
|
+
const content = message.content.map((block) => {
|
|
243
|
+
if (block?.type !== 'toolCall') return block
|
|
244
|
+
const normalized = normalizeHistoryToolCall(block)
|
|
245
|
+
if (block.id !== undefined && block.id !== null) toolCalls.set(String(block.id), normalized)
|
|
246
|
+
changed = true
|
|
247
|
+
return normalized.block
|
|
248
|
+
})
|
|
249
|
+
return changed ? { ...message, content } : message
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (message?.role === 'toolResult' && message.toolCallId !== undefined && message.toolCallId !== null) {
|
|
253
|
+
const normalized = toolCalls.get(String(message.toolCallId))
|
|
254
|
+
if (!normalized) return message
|
|
255
|
+
const details = message.details && typeof message.details === 'object' && !Array.isArray(message.details)
|
|
256
|
+
? { ...message.details, __quickforgeAcp: normalized.metadata }
|
|
257
|
+
: { __quickforgeAcp: normalized.metadata }
|
|
258
|
+
return { ...message, toolName: normalized.name, details }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return message
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function safeJson(value) {
|
|
266
|
+
try {
|
|
267
|
+
return JSON.stringify(value)
|
|
268
|
+
} catch {
|
|
269
|
+
return String(value ?? '')
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function boundedText(value, limit = TOOL_OUTPUT_LIMIT) {
|
|
274
|
+
const text = String(value ?? '')
|
|
275
|
+
return text.length > limit ? `${text.slice(0, limit)}\n…[truncated]` : text
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function safeRawOutput(value) {
|
|
279
|
+
const text = typeof value === 'string' ? value : safeJson(stripMeta(value))
|
|
280
|
+
return boundedText(text
|
|
281
|
+
.replace(/-----BEGIN[\s\S]*?PRIVATE KEY-----[\s\S]*?-----END[\s\S]*?PRIVATE KEY-----/gi, '[private key]')
|
|
282
|
+
.replace(/\bBearer\s+[^\s,;"}]+/gi, 'Bearer [redacted]')
|
|
283
|
+
.replace(/(["']?(?:token|secret|password|api[-_ ]?key|private[-_ ]?key)["']?\s*[:=]\s*)["']?[^\s,;"}]+["']?/gi, '$1"[redacted]"'))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function diffDetails(item) {
|
|
287
|
+
const hasOldText = typeof item?.oldText === 'string'
|
|
288
|
+
const oldText = hasOldText ? item.oldText : ''
|
|
289
|
+
const newText = typeof item?.newText === 'string' ? item.newText : ''
|
|
290
|
+
const oldLines = oldText ? oldText.split('\n') : []
|
|
291
|
+
const newLines = newText ? newText.split('\n') : []
|
|
292
|
+
const path = typeof item?.path === 'string' ? item.path : ''
|
|
293
|
+
const text = hasOldText
|
|
294
|
+
? [
|
|
295
|
+
`--- ${path || 'before'}`,
|
|
296
|
+
`+++ ${path || 'after'}`,
|
|
297
|
+
...oldLines.map((line) => `-${line}`),
|
|
298
|
+
...newLines.map((line) => `+${line}`),
|
|
299
|
+
].join('\n')
|
|
300
|
+
: newText
|
|
301
|
+
return {
|
|
302
|
+
format: hasOldText ? 'unified' : 'raw',
|
|
303
|
+
path,
|
|
304
|
+
addedLines: newLines.length,
|
|
305
|
+
removedLines: oldLines.length,
|
|
306
|
+
text: boundedText(text),
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function toolResult(update, fallbackMetadata = {}) {
|
|
311
|
+
const metadata = toolMetadata(update, fallbackMetadata)
|
|
312
|
+
const output = []
|
|
313
|
+
let diff
|
|
314
|
+
for (const item of Array.isArray(update?.content) ? update.content : []) {
|
|
315
|
+
if (item?.type === 'content') {
|
|
316
|
+
const block = item.content
|
|
317
|
+
if (block?.type === 'text' && typeof block.text === 'string') output.push(block.text)
|
|
318
|
+
else if (block?.type === 'resource' && typeof block.resource?.text === 'string') output.push(block.resource.text)
|
|
319
|
+
else if (block?.type === 'resource' && typeof block.resource?.blob === 'string') {
|
|
320
|
+
output.push(`[binary resource: ${block.resource.mimeType || 'application/octet-stream'}, ${block.resource.blob.length} base64 chars]`)
|
|
321
|
+
}
|
|
322
|
+
} else if (item?.type === 'diff') {
|
|
323
|
+
const nextDiff = diffDetails(item)
|
|
324
|
+
if (!diff) diff = nextDiff
|
|
325
|
+
output.push(`[diff: ${nextDiff.path || 'unknown path'}, +${nextDiff.addedLines} -${nextDiff.removedLines}]`)
|
|
326
|
+
} else if (item?.type === 'terminal' && typeof item.terminalId === 'string') {
|
|
327
|
+
output.push(`[terminal: ${item.terminalId}]`)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (output.length === 0 && update?.rawOutput !== undefined) output.push(safeRawOutput(update.rawOutput))
|
|
331
|
+
const value = boundedText(output.filter(Boolean).join('\n'))
|
|
332
|
+
return {
|
|
333
|
+
content: value ? [{ type: 'text', text: value }] : [],
|
|
334
|
+
details: {
|
|
335
|
+
__quickforgeAcp: metadata,
|
|
336
|
+
...(value ? { output: value } : {}),
|
|
337
|
+
...(diff ? { diff } : {}),
|
|
338
|
+
},
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function baseAssistantMessage(content, id, stopReason = 'stop') {
|
|
343
|
+
return {
|
|
344
|
+
role: 'assistant',
|
|
345
|
+
content,
|
|
346
|
+
api: 'acp',
|
|
347
|
+
provider: 'opencode',
|
|
348
|
+
model: 'opencode',
|
|
349
|
+
usage: {
|
|
350
|
+
input: 0,
|
|
351
|
+
output: 0,
|
|
352
|
+
cacheRead: 0,
|
|
353
|
+
cacheWrite: 0,
|
|
354
|
+
totalTokens: 0,
|
|
355
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
356
|
+
},
|
|
357
|
+
stopReason,
|
|
358
|
+
timestamp: Date.now(),
|
|
359
|
+
...(id ? { id } : {}),
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function assistantErrorMessage(text) {
|
|
364
|
+
return {
|
|
365
|
+
...baseAssistantMessage([], undefined, 'error'),
|
|
366
|
+
errorMessage: text,
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function sanitizeOpenCodeDiagnostic(value) {
|
|
371
|
+
return String(value ?? '')
|
|
372
|
+
.replace(/-----BEGIN[\s\S]*?PRIVATE KEY-----[\s\S]*?-----END[\s\S]*?PRIVATE KEY-----/gi, '[private key]')
|
|
373
|
+
.replace(/\bBearer\s+[^\s,;]+/gi, 'Bearer [redacted]')
|
|
374
|
+
.replace(/\b(token|secret|password|api[-_ ]?key|private[-_ ]?key)\b\s*[:=]\s*[^\s,;]+/gi, '$1=[redacted]')
|
|
375
|
+
.replace(/\b(?:https?|file):\/\/[^\s)\]}]+/gi, '[url]')
|
|
376
|
+
.replace(/\b[A-Za-z]:\\(?:[^\s<>:"|?*]+\\)*[^\s<>:"|?*]*/g, '[local path]')
|
|
377
|
+
.replace(/(^|[\s(])\/(?:Users|home|var|tmp|private|opt|etc)\/[^\s)\]}]+/g, '$1[local path]')
|
|
378
|
+
.replace(/\s+/g, ' ')
|
|
379
|
+
.trim()
|
|
380
|
+
.slice(0, DIAGNOSTIC_LIMIT)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function authRequiredError(error) {
|
|
384
|
+
return error?.code === -32000 || /auth(?:entication|orization)?\s+(?:is\s+)?required|not\s+authenticated|login required/i.test(error?.message || '')
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function publicAcpError(error, stage, fallback = 'OpenCode ACP request failed.') {
|
|
388
|
+
if (error?.errorCode && error?.statusCode) return error
|
|
389
|
+
if (authRequiredError(error)) {
|
|
390
|
+
return Object.assign(new Error('OpenCode authentication is required. Complete OpenCode login in a terminal, then try again.'), {
|
|
391
|
+
statusCode: 401,
|
|
392
|
+
errorCode: 'OPENCODE_AUTH_REQUIRED',
|
|
393
|
+
stage,
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
const detail = sanitizeOpenCodeDiagnostic(error?.message)
|
|
397
|
+
return Object.assign(new Error(detail || fallback), {
|
|
398
|
+
statusCode: error?.statusCode || 503,
|
|
399
|
+
errorCode: error?.errorCode || 'OPENCODE_ACP_RUNTIME_ERROR',
|
|
400
|
+
stage,
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function timeoutError(stage, timeoutMs) {
|
|
405
|
+
return Object.assign(new Error(`OpenCode ACP ${stage} timed out after ${timeoutMs}ms.`), {
|
|
406
|
+
statusCode: 504,
|
|
407
|
+
errorCode: 'OPENCODE_ACP_TIMEOUT',
|
|
408
|
+
stage,
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function incompatibleError(message, stage = 'initialize') {
|
|
413
|
+
return Object.assign(new Error(message), {
|
|
414
|
+
statusCode: 503,
|
|
415
|
+
errorCode: 'OPENCODE_ACP_INCOMPATIBLE',
|
|
416
|
+
stage,
|
|
417
|
+
})
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function capabilityEnabled(value) {
|
|
421
|
+
return value !== undefined && value !== null && value !== false
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function optionalString(value) {
|
|
425
|
+
return typeof value === 'string' ? value : undefined
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function sanitizeConfigSelectOption(option) {
|
|
429
|
+
if (!option || typeof option.value !== 'string' || typeof option.name !== 'string') return null
|
|
430
|
+
return {
|
|
431
|
+
value: option.value,
|
|
432
|
+
name: option.name,
|
|
433
|
+
...(optionalString(option.description) !== undefined ? { description: option.description } : {}),
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function sanitizeConfigSelectOptions(options) {
|
|
438
|
+
if (!Array.isArray(options)) return []
|
|
439
|
+
return options.map((option) => {
|
|
440
|
+
if (option && typeof option.group === 'string' && typeof option.name === 'string' && Array.isArray(option.options)) {
|
|
441
|
+
return {
|
|
442
|
+
group: option.group,
|
|
443
|
+
name: option.name,
|
|
444
|
+
options: option.options.map(sanitizeConfigSelectOption).filter(Boolean),
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return sanitizeConfigSelectOption(option)
|
|
448
|
+
}).filter(Boolean)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function sanitizeConfigOption(option) {
|
|
452
|
+
if (!option || typeof option.id !== 'string' || typeof option.name !== 'string') return null
|
|
453
|
+
const common = {
|
|
454
|
+
id: option.id,
|
|
455
|
+
name: option.name,
|
|
456
|
+
...(optionalString(option.description) !== undefined ? { description: option.description } : {}),
|
|
457
|
+
...(optionalString(option.category) !== undefined ? { category: option.category } : {}),
|
|
458
|
+
}
|
|
459
|
+
if (option.type === 'boolean' && typeof option.currentValue === 'boolean') {
|
|
460
|
+
return { ...common, type: 'boolean', currentValue: option.currentValue }
|
|
461
|
+
}
|
|
462
|
+
if (option.type === 'select' && typeof option.currentValue === 'string') {
|
|
463
|
+
return { ...common, type: 'select', currentValue: option.currentValue, options: sanitizeConfigSelectOptions(option.options) }
|
|
464
|
+
}
|
|
465
|
+
return null
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function sanitizeConfigOptions(configOptions) {
|
|
469
|
+
return Array.isArray(configOptions) ? configOptions.map(sanitizeConfigOption).filter(Boolean) : []
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function sanitizeModes(modes) {
|
|
473
|
+
if (!modes || typeof modes.currentModeId !== 'string' || !Array.isArray(modes.availableModes)) return null
|
|
474
|
+
return {
|
|
475
|
+
currentModeId: modes.currentModeId,
|
|
476
|
+
availableModes: modes.availableModes.map((mode) => {
|
|
477
|
+
if (!mode || typeof mode.id !== 'string' || typeof mode.name !== 'string') return null
|
|
478
|
+
return {
|
|
479
|
+
id: mode.id,
|
|
480
|
+
name: mode.name,
|
|
481
|
+
...(optionalString(mode.description) !== undefined ? { description: mode.description } : {}),
|
|
482
|
+
}
|
|
483
|
+
}).filter(Boolean),
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function sanitizeAvailableCommands(availableCommands) {
|
|
488
|
+
if (!Array.isArray(availableCommands)) return []
|
|
489
|
+
return availableCommands.map((command) => {
|
|
490
|
+
if (!command || typeof command.name !== 'string' || typeof command.description !== 'string') return null
|
|
491
|
+
const input = command.input && typeof command.input.hint === 'string' ? { hint: command.input.hint } : undefined
|
|
492
|
+
return { name: command.name, description: command.description, ...(input ? { input } : {}) }
|
|
493
|
+
}).filter(Boolean)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function sanitizeSessionInfo(update) {
|
|
497
|
+
const result = {}
|
|
498
|
+
if (update?.title === null || typeof update?.title === 'string') result.title = update.title
|
|
499
|
+
if (update?.updatedAt === null || typeof update?.updatedAt === 'string') result.updatedAt = update.updatedAt
|
|
500
|
+
return result
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function sanitizeUsage(update) {
|
|
504
|
+
if (!Number.isFinite(update?.used) || !Number.isFinite(update?.size)) return null
|
|
505
|
+
const usage = { used: update.used, size: update.size }
|
|
506
|
+
if (update.cost && Number.isFinite(update.cost.amount) && typeof update.cost.currency === 'string') {
|
|
507
|
+
usage.cost = { amount: update.cost.amount, currency: update.cost.currency }
|
|
508
|
+
} else if (update?.cost === null) {
|
|
509
|
+
usage.cost = null
|
|
510
|
+
}
|
|
511
|
+
return usage
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function configOptionValues(option) {
|
|
515
|
+
if (option?.type !== 'select' || !Array.isArray(option.options)) return []
|
|
516
|
+
return option.options.flatMap((item) => Array.isArray(item?.options) ? item.options : [item]).map((item) => item?.value).filter((value) => typeof value === 'string')
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function normalizeOpenCodeSessionSetupResult(result) {
|
|
520
|
+
return {
|
|
521
|
+
configOptions: sanitizeConfigOptions(result?.configOptions),
|
|
522
|
+
modes: sanitizeModes(result?.modes),
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export class OpenCodeAcpAgent {
|
|
527
|
+
constructor({
|
|
528
|
+
sessionId,
|
|
529
|
+
cwd,
|
|
530
|
+
messages = [],
|
|
531
|
+
harnessSessionId = null,
|
|
532
|
+
sourceHarnessSessionId = null,
|
|
533
|
+
restoredUsage = null,
|
|
534
|
+
requestPermission,
|
|
535
|
+
logger,
|
|
536
|
+
timeouts = {},
|
|
537
|
+
dependencies = {},
|
|
538
|
+
}) {
|
|
539
|
+
this.sessionId = sessionId
|
|
540
|
+
this.cwd = cwd
|
|
541
|
+
this.harnessSessionId = harnessSessionId
|
|
542
|
+
this.sourceHarnessSessionId = sourceHarnessSessionId
|
|
543
|
+
this.restoredUsage = sanitizeUsage(restoredUsage)
|
|
544
|
+
this.requestPermission = requestPermission
|
|
545
|
+
this.logger = logger
|
|
546
|
+
this.timeouts = { ...DEFAULT_OPENCODE_ACP_TIMEOUTS, ...timeouts }
|
|
547
|
+
this.dependencies = {
|
|
548
|
+
resolveCommand: resolveOpenCodeCommand,
|
|
549
|
+
spawnOpenCode,
|
|
550
|
+
terminateProcessTree,
|
|
551
|
+
setTimer: setTimeout,
|
|
552
|
+
clearTimer: clearTimeout,
|
|
553
|
+
...dependencies,
|
|
554
|
+
}
|
|
555
|
+
this.listeners = new Set()
|
|
556
|
+
this.toolCalls = new Map()
|
|
557
|
+
this.acceptUpdates = false
|
|
558
|
+
this.disposed = false
|
|
559
|
+
this.connection = null
|
|
560
|
+
this.context = null
|
|
561
|
+
this.process = null
|
|
562
|
+
this.stderr = ''
|
|
563
|
+
this.idleWaiters = new Set()
|
|
564
|
+
this.abortController = null
|
|
565
|
+
this.signal = undefined
|
|
566
|
+
this.currentPromptPromise = null
|
|
567
|
+
this.promptRunSequence = 0
|
|
568
|
+
this.activePromptRunId = 0
|
|
569
|
+
this.runtimeFailure = null
|
|
570
|
+
this.runtimeFailureHandled = false
|
|
571
|
+
this.runtimeTerminationPromise = null
|
|
572
|
+
this.activeRunEnded = false
|
|
573
|
+
this.initialized = false
|
|
574
|
+
this.initializeResult = null
|
|
575
|
+
this.protocolVersion = null
|
|
576
|
+
this.agentInfo = null
|
|
577
|
+
this.agentCapabilities = {}
|
|
578
|
+
this.authMethods = []
|
|
579
|
+
this.setupMetadataBuffer = []
|
|
580
|
+
this.runtimeFailurePromise = new Promise((resolve) => { this.resolveRuntimeFailure = resolve })
|
|
581
|
+
this.state = {
|
|
582
|
+
systemPrompt: '',
|
|
583
|
+
model: null,
|
|
584
|
+
thinkingLevel: 'off',
|
|
585
|
+
messages: normalizeOpenCodeHistoryMessages(messages),
|
|
586
|
+
tools: [],
|
|
587
|
+
isStreaming: false,
|
|
588
|
+
streamingMessage: undefined,
|
|
589
|
+
pendingToolCalls: new Set(),
|
|
590
|
+
errorMessage: undefined,
|
|
591
|
+
acpSession: {
|
|
592
|
+
configOptions: [],
|
|
593
|
+
modes: null,
|
|
594
|
+
availableCommands: [],
|
|
595
|
+
sessionInfo: {},
|
|
596
|
+
usage: this.restoredUsage,
|
|
597
|
+
},
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async initialize() {
|
|
602
|
+
const command = await this.dependencies.resolveCommand()
|
|
603
|
+
const child = this.dependencies.spawnOpenCode(command, this.cwd, this.dependencies.spawnImpl, this.dependencies.platform)
|
|
604
|
+
this.process = child
|
|
605
|
+
child.stdin?.on('error', () => {})
|
|
606
|
+
child.stdout?.on('error', () => {})
|
|
607
|
+
child.stderr?.setEncoding('utf8')
|
|
608
|
+
child.stderr?.on('data', (chunk) => {
|
|
609
|
+
this.stderr = sanitizeOpenCodeDiagnostic(`${this.stderr}${chunk}`).slice(-DIAGNOSTIC_LIMIT)
|
|
610
|
+
})
|
|
611
|
+
child.once('error', (error) => this.handleRuntimeFailure(error, 'process'))
|
|
612
|
+
child.once('exit', (code, signal) => {
|
|
613
|
+
if (this.disposed) return
|
|
614
|
+
const exitReason = signal || (code ?? 'unknown')
|
|
615
|
+
const detail = this.stderr.trim()
|
|
616
|
+
this.handleRuntimeFailure(new Error(`OpenCode ACP process exited (${exitReason}).${detail ? ` ${detail}` : ''}`), 'process')
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
const app = client({ name: ACP_CLIENT_NAME })
|
|
620
|
+
.onRequest(methods.client.session.requestPermission, ({ params }) => this.handlePermissionRequest(params))
|
|
621
|
+
.onNotification(methods.client.session.update, ({ params }) => this.handleSessionUpdate(params))
|
|
622
|
+
.onRequest(methods.client.fs.readTextFile, async () => { throw new Error('OpenCode must use its native filesystem tools.') })
|
|
623
|
+
.onRequest(methods.client.fs.writeTextFile, async () => { throw new Error('OpenCode must use its native filesystem tools.') })
|
|
624
|
+
|
|
625
|
+
const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout))
|
|
626
|
+
const connection = app.connect(stream)
|
|
627
|
+
this.connection = connection
|
|
628
|
+
void connection.closed.then(
|
|
629
|
+
() => this.handleRuntimeFailure(new Error('OpenCode ACP connection closed.'), 'transport'),
|
|
630
|
+
(error) => this.handleRuntimeFailure(error, 'transport'),
|
|
631
|
+
)
|
|
632
|
+
this.context = connection.agent
|
|
633
|
+
|
|
634
|
+
const initialized = await this.requestWithDeadline('initialize', this.timeouts.initialize, () => this.context.request(methods.agent.initialize, {
|
|
635
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
636
|
+
clientCapabilities: {},
|
|
637
|
+
clientInfo: { name: ACP_CLIENT_NAME, version: '1' },
|
|
638
|
+
}))
|
|
639
|
+
this.validateInitializeResult(initialized)
|
|
640
|
+
this.initialized = true
|
|
641
|
+
|
|
642
|
+
if (this.harnessSessionId) {
|
|
643
|
+
await this.loadExistingSession()
|
|
644
|
+
} else if (this.sourceHarnessSessionId) {
|
|
645
|
+
this.requireCapability(this.supportsSessionCapability('fork'), 'OpenCode does not advertise ACP session/fork support.', 'session/fork')
|
|
646
|
+
const forked = await this.requestWithDeadline('session/fork', this.timeouts.sessionSetup, () => this.context.request(methods.agent.session.fork, {
|
|
647
|
+
sessionId: this.sourceHarnessSessionId,
|
|
648
|
+
cwd: this.cwd,
|
|
649
|
+
mcpServers: [],
|
|
650
|
+
}))
|
|
651
|
+
this.harnessSessionId = forked.sessionId
|
|
652
|
+
this.applySessionSetupResult(forked)
|
|
653
|
+
} else {
|
|
654
|
+
const created = await this.requestWithDeadline('session/new', this.timeouts.sessionSetup, () => this.context.request(methods.agent.session.new, {
|
|
655
|
+
cwd: this.cwd,
|
|
656
|
+
mcpServers: [],
|
|
657
|
+
}))
|
|
658
|
+
this.harnessSessionId = created.sessionId
|
|
659
|
+
this.applySessionSetupResult(created)
|
|
660
|
+
}
|
|
661
|
+
this.acceptUpdates = true
|
|
662
|
+
this.flushSetupMetadataBuffer()
|
|
663
|
+
return this
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
validateInitializeResult(result) {
|
|
667
|
+
if (!result || result.protocolVersion !== PROTOCOL_VERSION) {
|
|
668
|
+
throw incompatibleError(`OpenCode ACP protocol version ${result?.protocolVersion ?? 'unknown'} is incompatible with QuickForge protocol version ${PROTOCOL_VERSION}.`)
|
|
669
|
+
}
|
|
670
|
+
if (result.agentCapabilities != null && (typeof result.agentCapabilities !== 'object' || Array.isArray(result.agentCapabilities))) {
|
|
671
|
+
throw incompatibleError('OpenCode returned invalid ACP agent capabilities.')
|
|
672
|
+
}
|
|
673
|
+
if (result.authMethods != null && !Array.isArray(result.authMethods)) {
|
|
674
|
+
throw incompatibleError('OpenCode returned invalid ACP authentication methods.')
|
|
675
|
+
}
|
|
676
|
+
this.initializeResult = result
|
|
677
|
+
this.protocolVersion = result.protocolVersion
|
|
678
|
+
this.agentInfo = result.agentInfo ?? null
|
|
679
|
+
this.agentCapabilities = result.agentCapabilities ?? {}
|
|
680
|
+
this.authMethods = result.authMethods ?? []
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
supportsSessionCapability(name) {
|
|
684
|
+
return capabilityEnabled(this.agentCapabilities?.sessionCapabilities?.[name])
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
requireCapability(supported, message, stage) {
|
|
688
|
+
if (!supported) throw incompatibleError(message, stage)
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
async loadExistingSession() {
|
|
692
|
+
const params = { sessionId: this.harnessSessionId, cwd: this.cwd, mcpServers: [] }
|
|
693
|
+
let loadError
|
|
694
|
+
if (this.agentCapabilities?.loadSession === true) {
|
|
695
|
+
try {
|
|
696
|
+
const loaded = await this.requestWithDeadline('session/load', this.timeouts.sessionSetup, () => this.context.request(methods.agent.session.load, params))
|
|
697
|
+
this.applySessionSetupResult(loaded)
|
|
698
|
+
return
|
|
699
|
+
} catch (error) {
|
|
700
|
+
loadError = error
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (this.supportsSessionCapability('resume')) {
|
|
704
|
+
try {
|
|
705
|
+
const resumed = await this.requestWithDeadline('session/resume', this.timeouts.sessionSetup, () => this.context.request(methods.agent.session.resume, params))
|
|
706
|
+
this.applySessionSetupResult(resumed)
|
|
707
|
+
return
|
|
708
|
+
} catch (error) {
|
|
709
|
+
throw publicAcpError(error, 'session/resume', `OpenCode session ${this.harnessSessionId} could not be restored.`)
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (loadError) throw publicAcpError(loadError, 'session/load', `OpenCode session ${this.harnessSessionId} could not be restored.`)
|
|
713
|
+
throw incompatibleError('OpenCode does not advertise ACP session/load or session/resume support.', 'session/restore')
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
applySessionSetupResult(result) {
|
|
717
|
+
const normalized = normalizeOpenCodeSessionSetupResult(result)
|
|
718
|
+
this.state.acpSession.configOptions = normalized.configOptions
|
|
719
|
+
this.state.acpSession.modes = normalized.modes
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
bufferSetupMetadataUpdate(params) {
|
|
723
|
+
if (params?.sessionId !== this.harnessSessionId && this.harnessSessionId) return
|
|
724
|
+
if (!ACP_METADATA_UPDATE_TYPES.has(params?.update?.sessionUpdate)) return
|
|
725
|
+
if (this.setupMetadataBuffer.length >= SETUP_METADATA_BUFFER_LIMIT) this.setupMetadataBuffer.shift()
|
|
726
|
+
this.setupMetadataBuffer.push(params)
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
flushSetupMetadataBuffer() {
|
|
730
|
+
const buffered = this.setupMetadataBuffer
|
|
731
|
+
this.setupMetadataBuffer = []
|
|
732
|
+
for (const params of buffered) this.handleSessionUpdate(params)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
clearSetupMetadataBuffer() {
|
|
736
|
+
this.setupMetadataBuffer = []
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
applyMetadataUpdate(update) {
|
|
740
|
+
switch (update?.sessionUpdate) {
|
|
741
|
+
case 'available_commands_update':
|
|
742
|
+
this.state.acpSession.availableCommands = sanitizeAvailableCommands(update.availableCommands)
|
|
743
|
+
return true
|
|
744
|
+
case 'current_mode_update': {
|
|
745
|
+
if (typeof update.currentModeId !== 'string') return true
|
|
746
|
+
const modes = this.state.acpSession.modes
|
|
747
|
+
this.state.acpSession.modes = modes
|
|
748
|
+
? { ...modes, currentModeId: update.currentModeId }
|
|
749
|
+
: { currentModeId: update.currentModeId, availableModes: [] }
|
|
750
|
+
return true
|
|
751
|
+
}
|
|
752
|
+
case 'config_option_update':
|
|
753
|
+
this.state.acpSession.configOptions = sanitizeConfigOptions(update.configOptions)
|
|
754
|
+
return true
|
|
755
|
+
case 'session_info_update':
|
|
756
|
+
this.state.acpSession.sessionInfo = { ...this.state.acpSession.sessionInfo, ...sanitizeSessionInfo(update) }
|
|
757
|
+
return true
|
|
758
|
+
case 'usage_update':
|
|
759
|
+
this.state.acpSession.usage = sanitizeUsage(update)
|
|
760
|
+
this.emit({ type: 'acp_session_usage_update', sessionId: this.harnessSessionId, usage: this.state.acpSession.usage })
|
|
761
|
+
return true
|
|
762
|
+
default:
|
|
763
|
+
return false
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
requestWithDeadline(stage, timeoutMs, request, options = {}) {
|
|
768
|
+
if (this.runtimeFailure && options.includeRuntimeFailure !== false) return Promise.reject(this.runtimeFailure)
|
|
769
|
+
let timer
|
|
770
|
+
let deadlineError = null
|
|
771
|
+
const timeout = new Promise((_, reject) => {
|
|
772
|
+
timer = this.dependencies.setTimer(() => {
|
|
773
|
+
deadlineError = timeoutError(stage, timeoutMs)
|
|
774
|
+
reject(deadlineError)
|
|
775
|
+
}, timeoutMs)
|
|
776
|
+
timer?.unref?.()
|
|
777
|
+
})
|
|
778
|
+
const contenders = [Promise.resolve().then(request), timeout]
|
|
779
|
+
if (options.includeRuntimeFailure !== false) {
|
|
780
|
+
contenders.push(this.runtimeFailurePromise.then((error) => Promise.reject(error)))
|
|
781
|
+
}
|
|
782
|
+
return Promise.race(contenders)
|
|
783
|
+
.catch((error) => {
|
|
784
|
+
const publicError = publicAcpError(error, stage)
|
|
785
|
+
if (error === deadlineError && options.fatal !== false) this.handleRuntimeFailure(publicError, stage)
|
|
786
|
+
throw publicError
|
|
787
|
+
})
|
|
788
|
+
.finally(() => this.dependencies.clearTimer(timer))
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
terminateFailedRuntime() {
|
|
792
|
+
if (this.runtimeTerminationPromise) return this.runtimeTerminationPromise
|
|
793
|
+
try { this.connection?.close() } catch { /* best-effort transport cleanup */ }
|
|
794
|
+
this.runtimeTerminationPromise = Promise.resolve(this.dependencies.terminateProcessTree(this.process)).catch(() => {})
|
|
795
|
+
return this.runtimeTerminationPromise
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
subscribe(listener) {
|
|
799
|
+
this.listeners.add(listener)
|
|
800
|
+
return () => this.listeners.delete(listener)
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
emit(event) {
|
|
804
|
+
for (const listener of this.listeners) {
|
|
805
|
+
try { listener(event) } catch { /* ignore listener failures */ }
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
validatePrompt(message) {
|
|
810
|
+
return attachmentPrompt(message, this.agentCapabilities)
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
async prompt(message) {
|
|
814
|
+
if (this.disposed) throw new Error('OpenCode session is closed.')
|
|
815
|
+
if (this.state.isStreaming) throw new Error('Generation is still running.')
|
|
816
|
+
if (this.runtimeFailure) throw this.runtimeFailure
|
|
817
|
+
const prompt = this.validatePrompt(message)
|
|
818
|
+
const text = messageText(message)
|
|
819
|
+
const userMessage = typeof message === 'string'
|
|
820
|
+
? { role: 'user', content: text, timestamp: Date.now() }
|
|
821
|
+
: message
|
|
822
|
+
const isInitialUserMessage = this.state.messages.length === 0
|
|
823
|
+
this.state.messages = [...this.state.messages, userMessage]
|
|
824
|
+
this.emit({ type: 'message_start', message: userMessage })
|
|
825
|
+
this.emit({ type: 'message_end', message: userMessage, isInitialUserMessage })
|
|
826
|
+
this.state.isStreaming = true
|
|
827
|
+
this.state.errorMessage = undefined
|
|
828
|
+
this.abortController = new AbortController()
|
|
829
|
+
const promptAbortController = this.abortController
|
|
830
|
+
this.signal = promptAbortController.signal
|
|
831
|
+
this.activeRunEnded = false
|
|
832
|
+
const runId = ++this.promptRunSequence
|
|
833
|
+
this.activePromptRunId = runId
|
|
834
|
+
this.emit({ type: 'agent_start' })
|
|
835
|
+
|
|
836
|
+
const run = async () => {
|
|
837
|
+
try {
|
|
838
|
+
const result = await this.requestWithDeadline('session/prompt', this.timeouts.prompt, () => this.context.request(methods.agent.session.prompt, {
|
|
839
|
+
sessionId: this.harnessSessionId,
|
|
840
|
+
prompt,
|
|
841
|
+
}))
|
|
842
|
+
if (promptAbortController.signal.aborted || this.activeRunEnded || this.activePromptRunId !== runId) return
|
|
843
|
+
this.finishAssistantMessage(result.stopReason)
|
|
844
|
+
this.state.isStreaming = false
|
|
845
|
+
this.signal = undefined
|
|
846
|
+
this.activeRunEnded = true
|
|
847
|
+
this.emit({ type: 'agent_end', messages: this.state.messages, stopReason: result.stopReason })
|
|
848
|
+
this.resolveIdleWaiters()
|
|
849
|
+
} catch (error) {
|
|
850
|
+
if (promptAbortController.signal.aborted || this.activePromptRunId !== runId) {
|
|
851
|
+
if (this.activePromptRunId !== runId) return
|
|
852
|
+
this.finishAssistantMessage()
|
|
853
|
+
this.clearRunState()
|
|
854
|
+
return
|
|
855
|
+
}
|
|
856
|
+
const publicError = publicAcpError(error, error?.stage || 'session/prompt', 'OpenCode ACP prompt failed.')
|
|
857
|
+
if (!this.activeRunEnded) this.failActiveRun(publicError)
|
|
858
|
+
throw publicError
|
|
859
|
+
} finally {
|
|
860
|
+
if (this.activePromptRunId === runId) this.currentPromptPromise = null
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
this.currentPromptPromise = run()
|
|
864
|
+
await this.currentPromptPromise
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
clearRunState() {
|
|
868
|
+
this.activePromptRunId = 0
|
|
869
|
+
this.currentPromptPromise = null
|
|
870
|
+
this.state.isStreaming = false
|
|
871
|
+
this.state.streamingMessage = undefined
|
|
872
|
+
this.state.pendingToolCalls = new Set()
|
|
873
|
+
this.toolCalls.clear()
|
|
874
|
+
this.signal = undefined
|
|
875
|
+
this.activeRunEnded = true
|
|
876
|
+
this.resolveIdleWaiters()
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
failActiveRun(error) {
|
|
880
|
+
if (this.activeRunEnded) return
|
|
881
|
+
this.finishAssistantMessage()
|
|
882
|
+
const errorMessage = error?.message || 'OpenCode ACP runtime failed.'
|
|
883
|
+
const last = this.state.messages[this.state.messages.length - 1]
|
|
884
|
+
if (!(last?.role === 'assistant' && last?.stopReason === 'error' && last?.errorMessage)) {
|
|
885
|
+
const message = assistantErrorMessage(errorMessage)
|
|
886
|
+
this.state.messages = [...this.state.messages, message]
|
|
887
|
+
this.emit({ type: 'message_start', message })
|
|
888
|
+
this.emit({ type: 'message_end', message })
|
|
889
|
+
}
|
|
890
|
+
this.state.isStreaming = false
|
|
891
|
+
this.state.streamingMessage = undefined
|
|
892
|
+
this.state.pendingToolCalls = new Set()
|
|
893
|
+
this.toolCalls.clear()
|
|
894
|
+
this.state.errorMessage = errorMessage
|
|
895
|
+
this.signal = undefined
|
|
896
|
+
this.activeRunEnded = true
|
|
897
|
+
this.emit({ type: 'error', error: errorMessage, errorCode: error?.errorCode, stage: error?.stage })
|
|
898
|
+
this.emit({ type: 'agent_end', messages: this.state.messages, errorMessage, stopReason: 'error' })
|
|
899
|
+
this.resolveIdleWaiters()
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
handleRuntimeFailure(error, stage = 'runtime') {
|
|
903
|
+
if (this.disposed || this.runtimeFailureHandled) return
|
|
904
|
+
this.runtimeFailureHandled = true
|
|
905
|
+
this.clearSetupMetadataBuffer()
|
|
906
|
+
const runtimeError = publicAcpError(error, stage, 'OpenCode runtime is unavailable.')
|
|
907
|
+
this.runtimeFailure = runtimeError
|
|
908
|
+
this.acceptUpdates = false
|
|
909
|
+
this.resolveRuntimeFailure(runtimeError)
|
|
910
|
+
if (this.state.isStreaming) this.failActiveRun(runtimeError)
|
|
911
|
+
else this.state.errorMessage = runtimeError.message
|
|
912
|
+
void this.terminateFailedRuntime()
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
appendAssistantChunk(update, type) {
|
|
916
|
+
const text = update?.content?.text || ''
|
|
917
|
+
if (!text) return
|
|
918
|
+
const current = this.state.streamingMessage
|
|
919
|
+
const id = String(update.messageId || current?.id || `opencode-${Date.now()}`)
|
|
920
|
+
if (current && current.id !== id) this.finishAssistantMessage()
|
|
921
|
+
const active = this.state.streamingMessage
|
|
922
|
+
const block = type === 'thinking' ? { type: 'thinking', thinking: text } : { type: 'text', text }
|
|
923
|
+
const content = active ? active.content.slice() : []
|
|
924
|
+
const last = content[content.length - 1]
|
|
925
|
+
if (type === 'thinking' && last?.type === 'thinking') last.thinking = `${last.thinking || ''}${text}`
|
|
926
|
+
else if (type === 'text' && last?.type === 'text') last.text = `${last.text || ''}${text}`
|
|
927
|
+
else content.push(block)
|
|
928
|
+
const next = baseAssistantMessage(content, id)
|
|
929
|
+
this.state.streamingMessage = next
|
|
930
|
+
this.emit({ type: active ? 'message_update' : 'message_start', message: next })
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
handleSessionUpdate(params) {
|
|
934
|
+
if (!this.acceptUpdates) {
|
|
935
|
+
if (!this.initialized || this.disposed || this.runtimeFailure) return
|
|
936
|
+
this.bufferSetupMetadataUpdate(params)
|
|
937
|
+
return
|
|
938
|
+
}
|
|
939
|
+
if (params?.sessionId !== this.harnessSessionId) return
|
|
940
|
+
const update = params.update
|
|
941
|
+
if (this.applyMetadataUpdate(update)) return
|
|
942
|
+
if (update?.sessionUpdate === 'agent_message_chunk' && update.content?.type === 'text') {
|
|
943
|
+
this.appendAssistantChunk(update, 'text')
|
|
944
|
+
return
|
|
945
|
+
}
|
|
946
|
+
if (update?.sessionUpdate === 'agent_thought_chunk' && update.content?.type === 'text') {
|
|
947
|
+
this.appendAssistantChunk(update, 'thinking')
|
|
948
|
+
return
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
if (update?.sessionUpdate === 'tool_call') {
|
|
952
|
+
this.finishAssistantMessage()
|
|
953
|
+
const id = String(update.toolCallId)
|
|
954
|
+
const name = toolName(update)
|
|
955
|
+
const metadata = toolMetadata(update)
|
|
956
|
+
const args = normalizedToolInput(update, metadata)
|
|
957
|
+
this.toolCalls.set(id, { name, input: args, metadata })
|
|
958
|
+
const message = baseAssistantMessage([{ type: 'toolCall', id, name, arguments: args }], update.messageId)
|
|
959
|
+
message.details = { __quickforgeAcp: metadata }
|
|
960
|
+
this.state.messages = [...this.state.messages, message]
|
|
961
|
+
this.emit({ type: 'message_start', message })
|
|
962
|
+
this.emit({ type: 'message_end', message })
|
|
963
|
+
this.state.pendingToolCalls = new Set([...this.state.pendingToolCalls, id])
|
|
964
|
+
this.emit({ type: 'tool_execution_start', toolCallId: id, toolName: name, args, details: { __quickforgeAcp: metadata } })
|
|
965
|
+
return
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (update?.sessionUpdate === 'tool_call_update') {
|
|
969
|
+
const id = String(update.toolCallId)
|
|
970
|
+
const previous = this.toolCalls.get(id)
|
|
971
|
+
if (!previous) return
|
|
972
|
+
const name = previous.name
|
|
973
|
+
const metadata = toolMetadata(update, previous.metadata)
|
|
974
|
+
previous.metadata = metadata
|
|
975
|
+
if (update.status === 'completed' || update.status === 'failed') {
|
|
976
|
+
this.state.pendingToolCalls = new Set([...this.state.pendingToolCalls].filter((item) => item !== id))
|
|
977
|
+
this.toolCalls.delete(id)
|
|
978
|
+
const result = toolResult(update, metadata)
|
|
979
|
+
this.state.messages = [...this.state.messages, {
|
|
980
|
+
role: 'toolResult',
|
|
981
|
+
toolCallId: id,
|
|
982
|
+
toolName: name,
|
|
983
|
+
content: result.content,
|
|
984
|
+
details: result.details,
|
|
985
|
+
isError: update.status === 'failed',
|
|
986
|
+
timestamp: Date.now(),
|
|
987
|
+
}]
|
|
988
|
+
this.emit({
|
|
989
|
+
type: 'tool_execution_end',
|
|
990
|
+
toolCallId: id,
|
|
991
|
+
toolName: name,
|
|
992
|
+
args: previous.input,
|
|
993
|
+
result,
|
|
994
|
+
isError: update.status === 'failed',
|
|
995
|
+
error: update.status === 'failed' ? sanitizeOpenCodeDiagnostic(update.rawOutput) : undefined,
|
|
996
|
+
details: { __quickforgeAcp: metadata },
|
|
997
|
+
})
|
|
998
|
+
} else {
|
|
999
|
+
this.emit({
|
|
1000
|
+
type: 'tool_execution_update',
|
|
1001
|
+
toolCallId: id,
|
|
1002
|
+
toolName: name,
|
|
1003
|
+
args: previous.input,
|
|
1004
|
+
partialResult: toolResult(update, metadata),
|
|
1005
|
+
details: { __quickforgeAcp: metadata },
|
|
1006
|
+
})
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
finishAssistantMessage(stopReason) {
|
|
1012
|
+
const current = this.state.streamingMessage
|
|
1013
|
+
if (!current) return
|
|
1014
|
+
const message = stopReason === undefined ? current : { ...current, stopReason }
|
|
1015
|
+
this.state.messages = [...this.state.messages, message]
|
|
1016
|
+
this.state.streamingMessage = undefined
|
|
1017
|
+
this.emit({ type: 'message_end', message })
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
async handlePermissionRequest(params) {
|
|
1021
|
+
if (!this.requestPermission) return { outcome: { outcome: 'cancelled' } }
|
|
1022
|
+
return this.requestPermission({
|
|
1023
|
+
toolCallId: String(params.toolCall?.toolCallId || ''),
|
|
1024
|
+
toolName: params.toolCall?.title || params.toolCall?.kind || 'OpenCode tool',
|
|
1025
|
+
args: params.toolCall?.rawInput,
|
|
1026
|
+
options: params.options || [],
|
|
1027
|
+
})
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
async setConfigOption(configId, value) {
|
|
1031
|
+
if (this.disposed) throw new Error('OpenCode session is closed.')
|
|
1032
|
+
if (this.state.isStreaming) throw Object.assign(new Error('Generation is still running. Stop it or wait until it finishes.'), { statusCode: 409 })
|
|
1033
|
+
const option = this.state.acpSession.configOptions.find((item) => item.id === configId)
|
|
1034
|
+
if (!option) throw Object.assign(new Error(`OpenCode did not advertise config option: ${configId}`), { statusCode: 400 })
|
|
1035
|
+
if (option.type === 'boolean') {
|
|
1036
|
+
if (typeof value !== 'boolean') throw Object.assign(new Error(`Config option ${configId} requires a boolean value.`), { statusCode: 400 })
|
|
1037
|
+
} else if (option.type === 'select') {
|
|
1038
|
+
if (typeof value !== 'string' || !configOptionValues(option).includes(value)) {
|
|
1039
|
+
throw Object.assign(new Error(`Invalid value for OpenCode config option: ${configId}`), { statusCode: 400 })
|
|
1040
|
+
}
|
|
1041
|
+
} else {
|
|
1042
|
+
throw Object.assign(new Error(`Unsupported OpenCode config option type: ${configId}`), { statusCode: 400 })
|
|
1043
|
+
}
|
|
1044
|
+
const params = {
|
|
1045
|
+
sessionId: this.harnessSessionId,
|
|
1046
|
+
configId,
|
|
1047
|
+
value,
|
|
1048
|
+
...(option.type === 'boolean' ? { type: 'boolean' } : {}),
|
|
1049
|
+
}
|
|
1050
|
+
const result = await this.requestWithDeadline('session/set_config_option', this.timeouts.sessionSetup, () => this.context.request(methods.agent.session.setConfigOption, params), { fatal: false })
|
|
1051
|
+
this.state.acpSession.configOptions = sanitizeConfigOptions(result?.configOptions)
|
|
1052
|
+
return this.state.acpSession
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
async setMode(modeId) {
|
|
1056
|
+
if (this.disposed) throw new Error('OpenCode session is closed.')
|
|
1057
|
+
if (this.state.isStreaming) throw Object.assign(new Error('Generation is still running. Stop it or wait until it finishes.'), { statusCode: 409 })
|
|
1058
|
+
const modes = this.state.acpSession.modes
|
|
1059
|
+
if (!modes?.availableModes.some((mode) => mode.id === modeId)) {
|
|
1060
|
+
throw Object.assign(new Error(`OpenCode did not advertise mode: ${modeId}`), { statusCode: 400 })
|
|
1061
|
+
}
|
|
1062
|
+
await this.requestWithDeadline('session/set_mode', this.timeouts.sessionSetup, () => this.context.request(methods.agent.session.setMode, {
|
|
1063
|
+
sessionId: this.harnessSessionId,
|
|
1064
|
+
modeId,
|
|
1065
|
+
}), { fatal: false })
|
|
1066
|
+
this.state.acpSession.modes = { ...modes, currentModeId: modeId }
|
|
1067
|
+
return this.state.acpSession
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
steer() {
|
|
1071
|
+
throw new Error('Steering is not supported by the OpenCode Harness.')
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
followUp() {
|
|
1075
|
+
throw new Error('Follow-up queuing is not supported by the OpenCode Harness.')
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
reset() {
|
|
1079
|
+
this.state.messages = []
|
|
1080
|
+
this.state.streamingMessage = undefined
|
|
1081
|
+
this.state.pendingToolCalls = new Set()
|
|
1082
|
+
this.state.errorMessage = undefined
|
|
1083
|
+
this.toolCalls.clear()
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
abort() {
|
|
1087
|
+
if (!this.state.isStreaming) return
|
|
1088
|
+
this.finishAssistantMessage()
|
|
1089
|
+
this.abortController?.abort()
|
|
1090
|
+
this.activePromptRunId = 0
|
|
1091
|
+
this.currentPromptPromise = null
|
|
1092
|
+
this.state.isStreaming = false
|
|
1093
|
+
this.state.streamingMessage = undefined
|
|
1094
|
+
this.state.pendingToolCalls = new Set()
|
|
1095
|
+
this.toolCalls.clear()
|
|
1096
|
+
this.signal = undefined
|
|
1097
|
+
this.activeRunEnded = true
|
|
1098
|
+
this.resolveIdleWaiters()
|
|
1099
|
+
if (this.context && this.harnessSessionId) {
|
|
1100
|
+
void this.context.notify(methods.agent.session.cancel, { sessionId: this.harnessSessionId }).catch(() => {})
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
waitForIdle() {
|
|
1105
|
+
if (!this.state.isStreaming) return Promise.resolve()
|
|
1106
|
+
if (this.currentPromptPromise) return this.currentPromptPromise.catch(() => {})
|
|
1107
|
+
return new Promise((resolve) => this.idleWaiters.add(resolve))
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
resolveIdleWaiters() {
|
|
1111
|
+
for (const resolve of this.idleWaiters) resolve()
|
|
1112
|
+
this.idleWaiters.clear()
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
async dispose() {
|
|
1116
|
+
if (this.disposed) return
|
|
1117
|
+
this.abort()
|
|
1118
|
+
this.disposed = true
|
|
1119
|
+
this.acceptUpdates = false
|
|
1120
|
+
this.clearSetupMetadataBuffer()
|
|
1121
|
+
try {
|
|
1122
|
+
if (this.context && this.harnessSessionId && this.supportsSessionCapability('close')) {
|
|
1123
|
+
await this.requestWithDeadline('session/close', this.timeouts.close, () => this.context.request(methods.agent.session.close, {
|
|
1124
|
+
sessionId: this.harnessSessionId,
|
|
1125
|
+
}), { includeRuntimeFailure: false, fatal: false }).catch(() => {})
|
|
1126
|
+
}
|
|
1127
|
+
} finally {
|
|
1128
|
+
this.connection?.close()
|
|
1129
|
+
await (this.runtimeTerminationPromise ?? Promise.resolve(this.dependencies.terminateProcessTree(this.process)).catch(() => {}))
|
|
1130
|
+
this.clearRunState()
|
|
1131
|
+
this.listeners.clear()
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
export async function createOpenCodeAcpAgent(options) {
|
|
1137
|
+
const agent = new OpenCodeAcpAgent(options)
|
|
1138
|
+
try {
|
|
1139
|
+
return await agent.initialize()
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
agent.acceptUpdates = false
|
|
1142
|
+
await agent.dispose().catch(() => {})
|
|
1143
|
+
if (error?.code === 'ENOENT') {
|
|
1144
|
+
throw Object.assign(new Error('OpenCode is unavailable. Install OpenCode and ensure the opencode command is on PATH.'), {
|
|
1145
|
+
statusCode: 503,
|
|
1146
|
+
errorCode: 'OPENCODE_UNAVAILABLE',
|
|
1147
|
+
})
|
|
1148
|
+
}
|
|
1149
|
+
throw publicAcpError(error, error?.stage || 'initialize')
|
|
1150
|
+
}
|
|
1151
|
+
}
|