@bybrawe/opencode-loop 0.5.27 → 0.5.29
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 +1 -1
- package/package.json +7 -4
- package/scripts/command-lifecycle-test.mjs +47 -0
- package/scripts/command-router-test.mjs +175 -0
- package/scripts/compaction-runtime-test.mjs +128 -0
- package/scripts/comprehensive-test.mjs +29 -7
- package/scripts/comprehensive-watchdog.mjs +32 -0
- package/scripts/executor-runtime-test.mjs +310 -0
- package/scripts/fixtures/opencode2-real-adapter-probe.js +70 -0
- package/scripts/goal-command-handlers-test.mjs +236 -0
- package/scripts/goal-policy-test.mjs +143 -0
- package/scripts/goal-runtime-test.mjs +203 -0
- package/scripts/goal-steering-test.mjs +153 -0
- package/scripts/host-adapter-contract-test.mjs +147 -0
- package/scripts/host-goal-steering-canary.mjs +443 -0
- package/scripts/host-loop-canary.mjs +68 -12
- package/scripts/job-workspace-test.mjs +164 -0
- package/scripts/loop-command-handlers-test.mjs +279 -0
- package/scripts/loop-registration-test.mjs +220 -0
- package/scripts/opencode2-host-canary.mjs +194 -0
- package/scripts/opencode2-loop-canary.mjs +433 -0
- package/scripts/runtime-contract-test.mjs +59 -0
- package/scripts/runtime-event-test.mjs +78 -0
- package/scripts/runtime-scope-test.mjs +46 -0
- package/scripts/scheduler-runtime-test.mjs +147 -0
- package/scripts/session-activity-test.mjs +95 -0
- package/scripts/session-manager-test.mjs +48 -0
- package/scripts/session-status-test.mjs +220 -0
- package/scripts/v2-command-adapter-test.mjs +113 -0
- package/scripts/v2-command-runtime-test.mjs +98 -0
- package/scripts/v2-event-bridge-test.mjs +72 -0
- package/scripts/v2-event-stream-contract-test.mjs +38 -0
- package/scripts/v2-host-contract-test.mjs +82 -0
- package/scripts/v2-plugin-sentinel-test.mjs +136 -0
- package/scripts/v2-prompt-interval-test.mjs +171 -0
- package/scripts/v2-prompt-runtime-test.mjs +267 -0
- package/scripts/v2-runtime-adapter-test.mjs +326 -0
- package/src/index.js +3737 -2220
- package/src/opencode/v1.js +1 -0
- package/src/source/core/args.js +281 -0
- package/src/source/core/jobs.js +80 -0
- package/src/source/core/process.js +51 -0
- package/src/source/core/state.js +211 -0
- package/src/source/legacy-v1.js +252 -0
- package/src/source/opencode/adapter.js +21 -0
- package/src/source/opencode/command-router.js +103 -0
- package/src/source/opencode/commands.js +83 -0
- package/src/source/opencode/goal-commands.js +107 -0
- package/src/source/opencode/host.js +134 -0
- package/src/source/opencode/loop-commands.js +195 -0
- package/src/source/opencode/loop-registration.js +100 -0
- package/src/source/opencode/messages.js +55 -0
- package/src/source/opencode/sdk.js +38 -0
- package/src/source/opencode/session-context.js +50 -0
- package/src/source/opencode2/capabilities.js +71 -0
- package/src/source/opencode2/commands.js +60 -0
- package/src/source/opencode2/event-bridge.js +174 -0
- package/src/source/opencode2/events.js +64 -0
- package/src/source/opencode2/experimental.js +47 -0
- package/src/source/opencode2/host-contract.js +94 -0
- package/src/source/opencode2/prompt-runtime.js +381 -0
- package/src/source/opencode2/runtime-adapter.js +56 -0
- package/src/source/opencode2/status.js +40 -0
- package/src/source/runtime/compaction.js +122 -0
- package/src/source/runtime/events.js +113 -0
- package/src/source/runtime/executor.js +558 -0
- package/src/source/runtime/goal-policy.js +77 -0
- package/src/source/runtime/goal-runtime.js +231 -0
- package/src/source/runtime/goal-steering.js +198 -0
- package/src/source/runtime/job-workspace.js +148 -0
- package/src/source/runtime/observer.js +23 -0
- package/src/source/runtime/scheduler.js +212 -0
- package/src/source/runtime/scope.js +56 -0
- package/src/source/runtime/session-activity.js +112 -0
- package/src/source/runtime/session-manager.js +100 -0
- package/src/source/runtime/session-registry.js +58 -0
- package/src/source/runtime/session-status.js +197 -0
- package/src/source/runtime/timers.js +45 -0
- package/src/source/v1.js +69 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export const OPENCODE_LOOP_V2_COMMANDS = Object.freeze({
|
|
2
|
+
loop: Object.freeze({
|
|
3
|
+
description: "Start an OpenCode auto-continue loop. Usage: /loop 5m <task>",
|
|
4
|
+
template: "OpenCode Loop local command handled. Reply exactly: OK.",
|
|
5
|
+
}),
|
|
6
|
+
"loop-pause": Object.freeze({
|
|
7
|
+
description: "Pause OpenCode Loop jobs.",
|
|
8
|
+
template: "OpenCode Loop pause command handled locally. Reply exactly: OK.",
|
|
9
|
+
}),
|
|
10
|
+
"loop-resume": Object.freeze({
|
|
11
|
+
description: "Resume OpenCode Loop jobs.",
|
|
12
|
+
template: "OpenCode Loop resume command handled locally. Reply exactly: OK.",
|
|
13
|
+
}),
|
|
14
|
+
"loop-stop": Object.freeze({
|
|
15
|
+
description: "Stop OpenCode Loop jobs.",
|
|
16
|
+
template: "OpenCode Loop stop command handled locally. Reply exactly: OK.",
|
|
17
|
+
}),
|
|
18
|
+
"loop-remove": Object.freeze({
|
|
19
|
+
description: "Remove OpenCode Loop jobs.",
|
|
20
|
+
template: "OpenCode Loop remove command handled locally. Reply exactly: OK.",
|
|
21
|
+
}),
|
|
22
|
+
"loop-clear": Object.freeze({
|
|
23
|
+
description: "Clear all OpenCode Loop jobs.",
|
|
24
|
+
template: "OpenCode Loop clear command handled locally. Reply exactly: OK.",
|
|
25
|
+
}),
|
|
26
|
+
"loop-status": Object.freeze({
|
|
27
|
+
description: "Show OpenCode Loop status for the current session.",
|
|
28
|
+
template: "OpenCode Loop status command handled locally. Reply exactly: OK.",
|
|
29
|
+
}),
|
|
30
|
+
"loop-now": Object.freeze({
|
|
31
|
+
description: "Run matching OpenCode Loop jobs on the next idle boundary.",
|
|
32
|
+
template: "OpenCode Loop run-now command handled locally. Reply exactly: OK.",
|
|
33
|
+
}),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export function registerOpenCode2LoopCommands(draft) {
|
|
37
|
+
if (!draft || typeof draft.update !== "function") {
|
|
38
|
+
throw new Error("OpenCode 2 command draft.update capability is unavailable")
|
|
39
|
+
}
|
|
40
|
+
for (const [name, definition] of Object.entries(OPENCODE_LOOP_V2_COMMANDS)) {
|
|
41
|
+
draft.update(name, (command) => {
|
|
42
|
+
command.template = definition.template
|
|
43
|
+
command.description = definition.description
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function parseOpenCode2LoopCommandText(value) {
|
|
49
|
+
if (typeof value !== "string") return undefined
|
|
50
|
+
for (const [name, definition] of Object.entries(OPENCODE_LOOP_V2_COMMANDS)) {
|
|
51
|
+
const template = definition.template
|
|
52
|
+
if (value === template) return Object.freeze({ name, arguments: "" })
|
|
53
|
+
if (!value.startsWith(`${template}\n\n`)) continue
|
|
54
|
+
return Object.freeze({
|
|
55
|
+
name,
|
|
56
|
+
arguments: value.slice(template.length + 2).trim(),
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
return undefined
|
|
60
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { normalizeOpenCodeEvent } from "../runtime/events.js"
|
|
2
|
+
import { createSessionRuntimeManager } from "../runtime/session-manager.js"
|
|
3
|
+
import { normalizeOpenCode2NativeEvent } from "./events.js"
|
|
4
|
+
|
|
5
|
+
function cleanupRegistration(registration) {
|
|
6
|
+
if (typeof registration === "function") return registration
|
|
7
|
+
if (!registration || typeof registration !== "object") return undefined
|
|
8
|
+
for (const key of ["dispose", "unsubscribe", "close"]) {
|
|
9
|
+
if (typeof registration[key] === "function") return registration[key].bind(registration)
|
|
10
|
+
}
|
|
11
|
+
return undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function eventStream(registration) {
|
|
15
|
+
const stream = registration?.stream ?? registration
|
|
16
|
+
if (!stream || typeof stream[Symbol.asyncIterator] !== "function") {
|
|
17
|
+
throw new TypeError("OpenCode 2 event subscribe must return an async event stream")
|
|
18
|
+
}
|
|
19
|
+
return stream
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sameDirectory(expected, actual) {
|
|
23
|
+
if (!expected || !actual) return true
|
|
24
|
+
return String(expected) === String(actual)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createOpenCode2EventBridge({
|
|
28
|
+
directory,
|
|
29
|
+
onEvent = async () => {},
|
|
30
|
+
onError = () => {},
|
|
31
|
+
runtimeManager = createSessionRuntimeManager(),
|
|
32
|
+
} = {}) {
|
|
33
|
+
if (typeof onEvent !== "function") throw new TypeError("OpenCode 2 event bridge requires onEvent to be a function")
|
|
34
|
+
if (typeof onError !== "function") throw new TypeError("OpenCode 2 event bridge requires onError to be a function")
|
|
35
|
+
|
|
36
|
+
let registration
|
|
37
|
+
let iterator
|
|
38
|
+
let iteratorClosed = false
|
|
39
|
+
let pump
|
|
40
|
+
let attached = false
|
|
41
|
+
let stopped = false
|
|
42
|
+
let disposed = false
|
|
43
|
+
let managerDisposed = false
|
|
44
|
+
let queue = Promise.resolve()
|
|
45
|
+
const sessionDirectories = new Map()
|
|
46
|
+
|
|
47
|
+
function report(error) {
|
|
48
|
+
try { onError(error) } catch {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function disposeManager(reason) {
|
|
52
|
+
if (managerDisposed) return false
|
|
53
|
+
managerDisposed = true
|
|
54
|
+
return runtimeManager.dispose(reason)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function closeIterator() {
|
|
58
|
+
if (iteratorClosed) return false
|
|
59
|
+
iteratorClosed = true
|
|
60
|
+
const current = iterator
|
|
61
|
+
iterator = undefined
|
|
62
|
+
if (typeof current?.return === "function") await current.return()
|
|
63
|
+
return Boolean(current)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalize(raw) {
|
|
67
|
+
const current = normalizeOpenCode2NativeEvent(raw) || normalizeOpenCodeEvent(raw)
|
|
68
|
+
if (!current) return undefined
|
|
69
|
+
const sessionID = current.sessionID
|
|
70
|
+
const rememberedDirectory = sessionID ? sessionDirectories.get(sessionID) : undefined
|
|
71
|
+
const eventDirectory = current.directory || rememberedDirectory || directory
|
|
72
|
+
const event = eventDirectory === current.directory
|
|
73
|
+
? current
|
|
74
|
+
: Object.freeze({ ...current, directory: eventDirectory })
|
|
75
|
+
if (sessionID && event.directory) sessionDirectories.set(sessionID, event.directory)
|
|
76
|
+
return event
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function process(raw) {
|
|
80
|
+
if (stopped) return undefined
|
|
81
|
+
const event = normalize(raw)
|
|
82
|
+
if (!event || !sameDirectory(directory, event.directory)) return undefined
|
|
83
|
+
|
|
84
|
+
const runtime = event.sessionID ? runtimeManager.observeExternal(event.sessionID) : undefined
|
|
85
|
+
await onEvent(event, runtime)
|
|
86
|
+
|
|
87
|
+
if (event.kind === "session" && event.action === "deleted") {
|
|
88
|
+
sessionDirectories.delete(event.sessionID)
|
|
89
|
+
runtimeManager.remove(event.sessionID, { expectedRuntime: runtime, reason: "session-deleted" })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (event.kind === "server" && event.action === "disposed") {
|
|
93
|
+
stopped = true
|
|
94
|
+
sessionDirectories.clear()
|
|
95
|
+
disposeManager("server-disposed")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return event
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function dispatch(raw) {
|
|
102
|
+
if (stopped) return Promise.resolve(undefined)
|
|
103
|
+
const result = queue.then(() => process(raw))
|
|
104
|
+
queue = result.catch(() => undefined)
|
|
105
|
+
return result
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function callback(raw) {
|
|
109
|
+
const pending = dispatch(raw)
|
|
110
|
+
pending.catch(report)
|
|
111
|
+
return pending.catch(() => undefined)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function consume(stream) {
|
|
115
|
+
const current = stream[Symbol.asyncIterator]()
|
|
116
|
+
iterator = current
|
|
117
|
+
iteratorClosed = false
|
|
118
|
+
try {
|
|
119
|
+
while (!stopped) {
|
|
120
|
+
const next = await current.next()
|
|
121
|
+
if (next?.done) break
|
|
122
|
+
await dispatch(next?.value)
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (!stopped) report(error)
|
|
126
|
+
} finally {
|
|
127
|
+
if (stopped && !iteratorClosed) await closeIterator().catch(() => undefined)
|
|
128
|
+
if (iterator === current) iterator = undefined
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function attach(subscribe) {
|
|
133
|
+
if (disposed) throw new Error("OpenCode 2 event bridge is disposed")
|
|
134
|
+
if (attached) throw new Error("OpenCode 2 event bridge is already attached")
|
|
135
|
+
if (typeof subscribe !== "function") throw new TypeError("OpenCode 2 event bridge requires an event subscribe function")
|
|
136
|
+
|
|
137
|
+
if (subscribe.length > 0) {
|
|
138
|
+
registration = await subscribe(callback)
|
|
139
|
+
attached = true
|
|
140
|
+
return registration
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
registration = await subscribe()
|
|
144
|
+
const stream = eventStream(registration)
|
|
145
|
+
attached = true
|
|
146
|
+
pump = consume(stream)
|
|
147
|
+
return registration
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function dispose(reason = "bridge-disposed") {
|
|
151
|
+
if (disposed) return false
|
|
152
|
+
disposed = true
|
|
153
|
+
stopped = true
|
|
154
|
+
await closeIterator().catch(() => undefined)
|
|
155
|
+
await pump?.catch(() => undefined)
|
|
156
|
+
await queue.catch(() => undefined)
|
|
157
|
+
|
|
158
|
+
const cleanup = cleanupRegistration(registration)
|
|
159
|
+
registration = undefined
|
|
160
|
+
if (cleanup) await cleanup()
|
|
161
|
+
sessionDirectories.clear()
|
|
162
|
+
disposeManager(reason)
|
|
163
|
+
return true
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return Object.freeze({
|
|
167
|
+
attach,
|
|
168
|
+
dispatch,
|
|
169
|
+
dispose,
|
|
170
|
+
runtimeManager,
|
|
171
|
+
isAttached: () => attached,
|
|
172
|
+
isDisposed: () => disposed,
|
|
173
|
+
})
|
|
174
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { parseOpenCode2LoopCommandText } from "./commands.js"
|
|
2
|
+
|
|
3
|
+
function record(value) {
|
|
4
|
+
return value && typeof value === "object" ? value : undefined
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function text(value) {
|
|
8
|
+
return typeof value === "string" && value.trim() ? value : undefined
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function directoryFrom(raw) {
|
|
12
|
+
return text(record(raw?.location)?.directory)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function sessionIDFrom(raw) {
|
|
16
|
+
const data = record(raw?.data)
|
|
17
|
+
return text(data?.sessionID)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function normalizeOpenCode2NativeEvent(raw) {
|
|
21
|
+
const type = text(raw?.type)
|
|
22
|
+
if (!type) return undefined
|
|
23
|
+
|
|
24
|
+
const data = record(raw?.data) || {}
|
|
25
|
+
const directory = directoryFrom(raw)
|
|
26
|
+
const sessionID = sessionIDFrom(raw)
|
|
27
|
+
|
|
28
|
+
if (type === "session.inbox.enqueued") {
|
|
29
|
+
const item = record(data.item)
|
|
30
|
+
const payload = record(item?.payload)
|
|
31
|
+
const parsed = item?.type === "user" ? parseOpenCode2LoopCommandText(payload?.text) : undefined
|
|
32
|
+
if (!sessionID || !parsed) return undefined
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
kind: "command",
|
|
35
|
+
action: "executed",
|
|
36
|
+
sessionID,
|
|
37
|
+
directory,
|
|
38
|
+
name: parsed.name,
|
|
39
|
+
arguments: parsed.arguments,
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (type === "session.execution.started") {
|
|
44
|
+
if (!sessionID) return undefined
|
|
45
|
+
return Object.freeze({ kind: "session", action: "status", sessionID, directory, status: "busy" })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (type === "session.execution.succeeded") {
|
|
49
|
+
if (!sessionID) return undefined
|
|
50
|
+
return Object.freeze({ kind: "session", action: "idle", sessionID, directory })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (type === "session.created") {
|
|
54
|
+
if (!sessionID) return undefined
|
|
55
|
+
return Object.freeze({ kind: "session", action: "created", sessionID, directory: directory || text(record(data.location)?.directory) })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (type === "session.deleted") {
|
|
59
|
+
if (!sessionID) return undefined
|
|
60
|
+
return Object.freeze({ kind: "session", action: "deleted", sessionID, directory })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { inspectOpenCode2Context } from "./capabilities.js"
|
|
2
|
+
import { OPENCODE_LOOP_V2_COMMANDS, registerOpenCode2LoopCommands } from "./commands.js"
|
|
3
|
+
import { createOpenCode2PromptRuntime } from "./prompt-runtime.js"
|
|
4
|
+
import { createOpenCode2RuntimeAdapter } from "./runtime-adapter.js"
|
|
5
|
+
|
|
6
|
+
export const OPENCODE_LOOP_V2_PLUGIN_ID = "bybrawe.opencode-loop.v2.experimental"
|
|
7
|
+
export { OPENCODE_LOOP_V2_COMMANDS }
|
|
8
|
+
|
|
9
|
+
export const OpenCodeLoopV2ExperimentalPlugin = {
|
|
10
|
+
id: OPENCODE_LOOP_V2_PLUGIN_ID,
|
|
11
|
+
async setup(ctx) {
|
|
12
|
+
const capabilities = inspectOpenCode2Context(ctx)
|
|
13
|
+
if (!capabilities.commandTransform) {
|
|
14
|
+
throw new Error("OpenCode 2 command.transform capability is unavailable")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const commandRegistration = await ctx.command.transform(registerOpenCode2LoopCommands)
|
|
18
|
+
if (!capabilities.eventSubscribe || !capabilities.sessionPrompt) {
|
|
19
|
+
await commandRegistration?.dispose?.()
|
|
20
|
+
return undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let promptRuntime
|
|
24
|
+
const onRuntimeEvent = async (event) => promptRuntime?.onEvent(event)
|
|
25
|
+
const runtime = createOpenCode2RuntimeAdapter(ctx, { onEvent: onRuntimeEvent })
|
|
26
|
+
promptRuntime = createOpenCode2PromptRuntime({
|
|
27
|
+
prompt: (request) => runtime.prompt(request),
|
|
28
|
+
command: capabilities.sessionCommand ? (request) => runtime.command(request) : undefined,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
await runtime.start()
|
|
33
|
+
} catch (error) {
|
|
34
|
+
await promptRuntime?.dispose?.().catch(() => undefined)
|
|
35
|
+
await commandRegistration?.dispose?.().catch(() => undefined)
|
|
36
|
+
throw error
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return async () => {
|
|
40
|
+
await promptRuntime?.dispose?.()
|
|
41
|
+
await runtime.dispose("plugin-cleanup")
|
|
42
|
+
await commandRegistration?.dispose?.()
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default OpenCodeLoopV2ExperimentalPlugin
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createOpenCode2EventBridge } from "./event-bridge.js"
|
|
2
|
+
|
|
3
|
+
export const OPENCODE_LOOP_V2_HOST_CONTRACT = "experimental"
|
|
4
|
+
|
|
5
|
+
function normalizePrompt(input) {
|
|
6
|
+
const sessionID = String(input?.sessionID || "").trim()
|
|
7
|
+
const text = String(input?.text || "")
|
|
8
|
+
if (!sessionID) throw new TypeError("prompt requires a session ID")
|
|
9
|
+
if (!text.trim()) throw new TypeError("prompt requires text")
|
|
10
|
+
return Object.freeze({
|
|
11
|
+
sessionID,
|
|
12
|
+
text,
|
|
13
|
+
agent: input?.agent,
|
|
14
|
+
model: input?.model,
|
|
15
|
+
noReply: input?.noReply === true,
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeCommand(input) {
|
|
20
|
+
const sessionID = String(input?.sessionID || "").trim()
|
|
21
|
+
const command = String(input?.command || "").trim().replace(/^\/+/, "")
|
|
22
|
+
if (!sessionID) throw new TypeError("command requires a session ID")
|
|
23
|
+
if (!command) throw new TypeError("command requires a command name")
|
|
24
|
+
return Object.freeze({
|
|
25
|
+
sessionID,
|
|
26
|
+
command,
|
|
27
|
+
arguments: input?.arguments === undefined ? undefined : String(input.arguments),
|
|
28
|
+
agent: input?.agent,
|
|
29
|
+
model: input?.model,
|
|
30
|
+
delivery: input?.delivery,
|
|
31
|
+
resume: input?.resume,
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createOpenCode2HostContract(options = {}) {
|
|
36
|
+
const onEvent = typeof options.onEvent === "function" ? options.onEvent : async () => {}
|
|
37
|
+
let started = false
|
|
38
|
+
let disposed = false
|
|
39
|
+
let hostDisposed = false
|
|
40
|
+
|
|
41
|
+
const bridge = createOpenCode2EventBridge({
|
|
42
|
+
directory: options.directory,
|
|
43
|
+
onError: options.onError,
|
|
44
|
+
onEvent: async (event, runtime) => {
|
|
45
|
+
if (event.kind === "server" && event.action === "disposed") hostDisposed = true
|
|
46
|
+
await onEvent(event, runtime)
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
async function start() {
|
|
51
|
+
if (disposed) throw new Error("OpenCode 2 host contract is disposed")
|
|
52
|
+
if (started) return false
|
|
53
|
+
if (typeof options.subscribe !== "function") throw new TypeError("subscribe must be a function")
|
|
54
|
+
if (typeof options.sendPrompt !== "function") throw new TypeError("sendPrompt must be a function")
|
|
55
|
+
await bridge.attach(options.subscribe)
|
|
56
|
+
started = true
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function prompt(input) {
|
|
61
|
+
if (disposed || hostDisposed) throw new Error("OpenCode 2 host contract is unavailable")
|
|
62
|
+
if (!started) throw new Error("OpenCode 2 host contract is not started")
|
|
63
|
+
const request = normalizePrompt(input)
|
|
64
|
+
const runtime = bridge.runtimeManager.observeExternal(request.sessionID)
|
|
65
|
+
return await options.sendPrompt(Object.freeze({ ...request, runtime }))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function command(input) {
|
|
69
|
+
if (disposed || hostDisposed) throw new Error("OpenCode 2 host contract is unavailable")
|
|
70
|
+
if (!started) throw new Error("OpenCode 2 host contract is not started")
|
|
71
|
+
if (typeof options.sendCommand !== "function") throw new Error("OpenCode 2 session.command capability is unavailable")
|
|
72
|
+
const request = normalizeCommand(input)
|
|
73
|
+
const runtime = bridge.runtimeManager.observeExternal(request.sessionID)
|
|
74
|
+
return await options.sendCommand(Object.freeze({ ...request, runtime }))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function dispose(reason = "host-contract-disposed") {
|
|
78
|
+
if (disposed) return false
|
|
79
|
+
disposed = true
|
|
80
|
+
await bridge.dispose(reason)
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return Object.freeze({
|
|
85
|
+
start,
|
|
86
|
+
prompt,
|
|
87
|
+
command,
|
|
88
|
+
dispose,
|
|
89
|
+
runtimeManager: bridge.runtimeManager,
|
|
90
|
+
isStarted: () => started,
|
|
91
|
+
isDisposed: () => disposed,
|
|
92
|
+
isHostDisposed: () => hostDisposed,
|
|
93
|
+
})
|
|
94
|
+
}
|