@bybrawe/opencode-loop 0.5.28 → 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 +4 -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 +11 -3
- 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-goal-steering-canary.mjs +443 -0
- package/scripts/host-loop-canary.mjs +54 -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-loop-canary.mjs +433 -0
- package/scripts/scheduler-runtime-test.mjs +147 -0
- package/scripts/session-activity-test.mjs +95 -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-host-contract-test.mjs +27 -0
- package/scripts/v2-plugin-sentinel-test.mjs +57 -8
- 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 +2751 -1594
- package/src/source/legacy-v1.js +168 -1860
- 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/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/opencode2/capabilities.js +3 -1
- package/src/source/opencode2/commands.js +60 -0
- package/src/source/opencode2/event-bridge.js +19 -1
- package/src/source/opencode2/events.js +64 -0
- package/src/source/opencode2/experimental.js +32 -1
- package/src/source/opencode2/host-contract.js +26 -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/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/scheduler.js +212 -0
- package/src/source/runtime/session-activity.js +112 -0
- package/src/source/runtime/session-status.js +197 -0
- package/scripts/host-bridge-parity-test.mjs +0 -22
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { inspectOpenCode2Context } from "./capabilities.js"
|
|
2
|
+
import { createOpenCode2HostContract } from "./host-contract.js"
|
|
3
|
+
|
|
4
|
+
function promptRequest(request) {
|
|
5
|
+
const value = {
|
|
6
|
+
sessionID: request.sessionID,
|
|
7
|
+
}
|
|
8
|
+
if (request.noReply === true) {
|
|
9
|
+
value.noReply = true
|
|
10
|
+
value.parts = [{ type: "text", text: request.text }]
|
|
11
|
+
} else {
|
|
12
|
+
value.text = request.text
|
|
13
|
+
}
|
|
14
|
+
if (request.agent !== undefined) value.agent = request.agent
|
|
15
|
+
if (request.model !== undefined) value.model = request.model
|
|
16
|
+
return value
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function commandRequest(request) {
|
|
20
|
+
const value = {
|
|
21
|
+
sessionID: request.sessionID,
|
|
22
|
+
command: request.command,
|
|
23
|
+
}
|
|
24
|
+
if (request.arguments !== undefined) value.arguments = request.arguments
|
|
25
|
+
if (request.agent !== undefined) value.agent = request.agent
|
|
26
|
+
if (request.model !== undefined) value.model = request.model
|
|
27
|
+
if (request.delivery !== undefined) value.delivery = request.delivery
|
|
28
|
+
if (request.resume !== undefined) value.resume = request.resume
|
|
29
|
+
return value
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createOpenCode2RuntimeAdapter(ctx, options = {}) {
|
|
33
|
+
const capabilities = inspectOpenCode2Context(ctx)
|
|
34
|
+
if (!capabilities.eventSubscribe) throw new Error("OpenCode 2 event.subscribe capability is unavailable")
|
|
35
|
+
if (!capabilities.sessionPrompt) throw new Error("OpenCode 2 session.prompt capability is unavailable")
|
|
36
|
+
|
|
37
|
+
const host = createOpenCode2HostContract({
|
|
38
|
+
directory: options.directory,
|
|
39
|
+
subscribe: ctx.event.subscribe.bind(ctx.event),
|
|
40
|
+
sendPrompt: (request) => ctx.session.prompt(promptRequest(request)),
|
|
41
|
+
sendCommand: capabilities.sessionCommand ? (request) => ctx.session.command(commandRequest(request)) : undefined,
|
|
42
|
+
onEvent: options.onEvent,
|
|
43
|
+
onError: options.onError,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return Object.freeze({
|
|
47
|
+
start: () => host.start(),
|
|
48
|
+
prompt: (request) => host.prompt(request),
|
|
49
|
+
command: (request) => host.command(request),
|
|
50
|
+
dispose: (reason = "runtime-adapter-disposed") => host.dispose(reason),
|
|
51
|
+
runtimeManager: host.runtimeManager,
|
|
52
|
+
isStarted: host.isStarted,
|
|
53
|
+
isDisposed: host.isDisposed,
|
|
54
|
+
isHostDisposed: host.isHostDisposed,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { durationToText } from "../core/args.js"
|
|
2
|
+
import { goalStatusText, isGoalJob, jobLabel } from "../core/jobs.js"
|
|
3
|
+
|
|
4
|
+
function dueAt(job, current) {
|
|
5
|
+
if (Number(job?.runNowRequestedAt || 0) > 0) return current
|
|
6
|
+
const intervalMs = Math.max(0, Number(job?.intervalMs || 0))
|
|
7
|
+
const lastRunAt = Number(job?.lastRunAt || 0)
|
|
8
|
+
if (intervalMs === 0) return current
|
|
9
|
+
if (lastRunAt > 0) return lastRunAt + intervalMs
|
|
10
|
+
if (job?.immediate === false) {
|
|
11
|
+
const createdAt = Date.parse(job?.createdAt || "")
|
|
12
|
+
return (Number.isFinite(createdAt) ? createdAt : current) + intervalMs
|
|
13
|
+
}
|
|
14
|
+
return current
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function formatOpenCode2LoopStatus(state, current = Date.now()) {
|
|
18
|
+
const jobs = Array.isArray(state?.jobs) ? state.jobs : []
|
|
19
|
+
const lines = jobs.length
|
|
20
|
+
? jobs.map((job, index) => {
|
|
21
|
+
const dueIn = Math.max(0, dueAt(job, current) - current)
|
|
22
|
+
const flags = [
|
|
23
|
+
isGoalJob(job) ? `goal:${goalStatusText(job)}` : undefined,
|
|
24
|
+
job.paused ? "paused" : "active",
|
|
25
|
+
Number(job.runNowRequestedAt || 0) > 0 ? "run-now" : undefined,
|
|
26
|
+
job.safe ? "safe" : undefined,
|
|
27
|
+
job.askNever ? "ask-never" : undefined,
|
|
28
|
+
job.noOverlap ? "no-overlap" : undefined,
|
|
29
|
+
job.checkpointOnly ? "checkpoint-only" : undefined,
|
|
30
|
+
job.gitCheckpoint ? "git-checkpoint" : undefined,
|
|
31
|
+
].filter(Boolean).join(",")
|
|
32
|
+
return `${index + 1}. ${job.id}${job.name ? ` (${job.name})` : ""}: ${jobLabel(job)} | runs=${job.runCount || 0} | failures=${job.failureCount || 0} | due in ${durationToText(dueIn)} | ${flags}`
|
|
33
|
+
})
|
|
34
|
+
: ["No active loop jobs."]
|
|
35
|
+
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
jobs,
|
|
38
|
+
text: `OpenCode loop status:\n${lines.join("\n")}`,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { now as defaultNow } from "../core/args.js"
|
|
2
|
+
import { appendLoopLog as defaultAppendLoopLog } from "../core/process.js"
|
|
3
|
+
import { compactSession as defaultCompactSession, log as defaultLog } from "../opencode/host.js"
|
|
4
|
+
import { sdkErrorMessage as defaultErrorMessage } from "../opencode/sdk.js"
|
|
5
|
+
|
|
6
|
+
export function createCompactionRuntime(options = {}) {
|
|
7
|
+
const activeRuns = options.activeRuns
|
|
8
|
+
if (!(activeRuns instanceof Map)) throw new TypeError("createCompactionRuntime requires activeRuns Map")
|
|
9
|
+
if (typeof options.finalizeActiveRun !== "function") throw new TypeError("createCompactionRuntime requires finalizeActiveRun")
|
|
10
|
+
|
|
11
|
+
const now = typeof options.now === "function" ? options.now : defaultNow
|
|
12
|
+
const appendLoopLog = typeof options.appendLoopLog === "function" ? options.appendLoopLog : defaultAppendLoopLog
|
|
13
|
+
const compactSession = typeof options.compactSession === "function" ? options.compactSession : defaultCompactSession
|
|
14
|
+
const log = typeof options.log === "function" ? options.log : defaultLog
|
|
15
|
+
const errorMessage = typeof options.errorMessage === "function" ? options.errorMessage : defaultErrorMessage
|
|
16
|
+
const finalizeActiveRun = options.finalizeActiveRun
|
|
17
|
+
const requests = new Map()
|
|
18
|
+
|
|
19
|
+
function begin(sessionID, jobId, resumeAfter = false) {
|
|
20
|
+
if (typeof sessionID !== "string" || typeof jobId !== "string") return undefined
|
|
21
|
+
const request = {
|
|
22
|
+
jobId,
|
|
23
|
+
resumeAfter: Boolean(resumeAfter),
|
|
24
|
+
requestedAt: now(),
|
|
25
|
+
startedAt: 0,
|
|
26
|
+
completedAt: 0,
|
|
27
|
+
}
|
|
28
|
+
requests.set(sessionID, request)
|
|
29
|
+
return request
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getPending(sessionID) {
|
|
33
|
+
return requests.get(sessionID)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function clear(sessionID) {
|
|
37
|
+
return requests.delete(sessionID)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function clearForActiveRun(sessionID, active) {
|
|
41
|
+
const pending = requests.get(sessionID)
|
|
42
|
+
if (!pending || !active || pending.jobId === active.jobId) requests.delete(sessionID)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isCompleted(sessionID, jobId) {
|
|
46
|
+
const pending = requests.get(sessionID)
|
|
47
|
+
return Boolean(pending && pending.jobId === jobId && pending.completedAt)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function start(directory, client, sessionID, jobId, model, resumeAfter = false) {
|
|
51
|
+
begin(sessionID, jobId, resumeAfter)
|
|
52
|
+
const ok = await compactSession(directory, client, sessionID, model)
|
|
53
|
+
if (!ok) clear(sessionID)
|
|
54
|
+
return ok
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function maybeCompact(directory, client, sessionID, job) {
|
|
58
|
+
const dueRuns = job.compactEveryRuns > 0 && (job.runCount || 0) > 0 && (job.runCount || 0) % job.compactEveryRuns === 0 && job.lastCompactRunCount !== job.runCount
|
|
59
|
+
const dueTime = job.compactEveryMs > 0 && (!job.lastCompactAt || now() - job.lastCompactAt >= job.compactEveryMs)
|
|
60
|
+
if (!dueRuns && !dueTime) return { job, started: false }
|
|
61
|
+
|
|
62
|
+
if (await start(directory, client, sessionID, job.id, job.model, true)) {
|
|
63
|
+
job.lastCompactAt = now()
|
|
64
|
+
job.lastCompactRunCount = job.runCount || 0
|
|
65
|
+
return { job, started: true }
|
|
66
|
+
}
|
|
67
|
+
return { job, started: false }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function noteStarted(directory, sessionID) {
|
|
71
|
+
const pending = requests.get(sessionID)
|
|
72
|
+
if (!pending) return false
|
|
73
|
+
if (!pending.startedAt) {
|
|
74
|
+
pending.startedAt = now()
|
|
75
|
+
requests.set(sessionID, pending)
|
|
76
|
+
await appendLoopLog(directory, "compact-started", {
|
|
77
|
+
sessionID,
|
|
78
|
+
job: pending.jobId,
|
|
79
|
+
resumeAfter: pending.resumeAfter,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function finalize(directory, client, sessionID) {
|
|
86
|
+
const pending = requests.get(sessionID)
|
|
87
|
+
const active = activeRuns.get(sessionID)
|
|
88
|
+
if (!pending || !active || pending.jobId !== active.jobId) return false
|
|
89
|
+
return await finalizeActiveRun(directory, client, sessionID)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function noteCompleted(directory, client, sessionID) {
|
|
93
|
+
const pending = requests.get(sessionID)
|
|
94
|
+
if (!pending) return false
|
|
95
|
+
pending.completedAt = now()
|
|
96
|
+
requests.set(sessionID, pending)
|
|
97
|
+
await appendLoopLog(directory, "compact-event", {
|
|
98
|
+
sessionID,
|
|
99
|
+
job: pending.jobId,
|
|
100
|
+
resumeAfter: pending.resumeAfter,
|
|
101
|
+
})
|
|
102
|
+
const timer = setTimeout(() => {
|
|
103
|
+
finalize(directory, client, sessionID)
|
|
104
|
+
.catch((error) => log(client, "error", "compaction finalization failed", { error: errorMessage(error) }))
|
|
105
|
+
}, 0)
|
|
106
|
+
timer.unref?.()
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
begin,
|
|
112
|
+
getPending,
|
|
113
|
+
clear,
|
|
114
|
+
clearForActiveRun,
|
|
115
|
+
isCompleted,
|
|
116
|
+
start,
|
|
117
|
+
maybeCompact,
|
|
118
|
+
noteStarted,
|
|
119
|
+
finalize,
|
|
120
|
+
noteCompleted,
|
|
121
|
+
}
|
|
122
|
+
}
|