opencode-agent-skill 9.0.0 → 10.0.0
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 +49 -0
- package/README.md +696 -675
- package/bin/ocskill.mjs +24 -0
- package/global-config/AGENTS.md +78 -163
- package/global-config/plugins/ues-router/index.js +413 -59
- package/global-config/plugins/ues-router/router.js +35 -0
- package/global-config/plugins/ues-router/runtime-guard.js +265 -0
- package/lib/benchmark-confidence.mjs +49 -11
- package/lib/eval-ablation.mjs +104 -0
- package/lib/eval-report.mjs +11 -0
- package/lib/eval-telemetry.mjs +3 -0
- package/lib/model-policy.mjs +6 -0
- package/lib/orchestrator-policy.mjs +99 -6
- package/lib/task-engine.mjs +146 -9
- package/package.json +4 -3
- package/scripts/eval-ablation.mjs +44 -0
- package/scripts/eval-matrix.mjs +13 -2
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { Plugin } from "@opencode/plugin"
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs"
|
|
3
3
|
import { fileURLToPath } from "node:url"
|
|
4
4
|
import path from "node:path"
|
|
5
5
|
import { spawnSync } from "node:child_process"
|
|
6
|
-
import { classifyIntent,
|
|
6
|
+
import { classifyIntent, routeSkillsForPolicy } from "./router.js"
|
|
7
7
|
import { destructiveShellRisk } from "./safety.js"
|
|
8
8
|
import { runtimeCapabilities } from "./capabilities.js"
|
|
9
|
+
import {
|
|
10
|
+
budgetToolResult,
|
|
11
|
+
classifyProviderFailure,
|
|
12
|
+
createRuntimeGuard,
|
|
13
|
+
providerRecoveryPlan,
|
|
14
|
+
progressWatchdogDecision,
|
|
15
|
+
stableRuntimeHash,
|
|
16
|
+
} from "./runtime-guard.js"
|
|
9
17
|
|
|
10
18
|
const CONFIG_FILE = path.resolve(
|
|
11
19
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
@@ -114,6 +122,40 @@ function taskHasWrites(task) {
|
|
|
114
122
|
)
|
|
115
123
|
}
|
|
116
124
|
|
|
125
|
+
function workspaceSignal(root) {
|
|
126
|
+
const scope = [
|
|
127
|
+
"--",
|
|
128
|
+
".",
|
|
129
|
+
":(exclude).ues-work",
|
|
130
|
+
":(exclude).ues-learning",
|
|
131
|
+
":(exclude).ues-dashboard",
|
|
132
|
+
":(exclude).ues-sandboxes",
|
|
133
|
+
":(exclude).ues-cache",
|
|
134
|
+
":(exclude).ues-traces",
|
|
135
|
+
]
|
|
136
|
+
const commands = [
|
|
137
|
+
["status", "--porcelain=v1", "--untracked-files=all", ...scope],
|
|
138
|
+
["diff", "--binary", "--no-ext-diff", ...scope],
|
|
139
|
+
["diff", "--cached", "--binary", "--no-ext-diff", ...scope],
|
|
140
|
+
]
|
|
141
|
+
const parts = []
|
|
142
|
+
for (const args of commands) {
|
|
143
|
+
const result = spawnSync("git", args, {
|
|
144
|
+
cwd: root,
|
|
145
|
+
encoding: "utf8",
|
|
146
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
147
|
+
})
|
|
148
|
+
if (result.status !== 0) return stableRuntimeHash("non-git:" + root)
|
|
149
|
+
parts.push(result.stdout || "")
|
|
150
|
+
}
|
|
151
|
+
return stableRuntimeHash(parts.join("\n---UES-WORKSPACE-SIGNAL---\n"))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function sessionContextDigest(messages) {
|
|
155
|
+
const recent = Array.isArray(messages) ? messages.slice(-12) : messages
|
|
156
|
+
return stableRuntimeHash(recent || [])
|
|
157
|
+
}
|
|
158
|
+
|
|
117
159
|
function policySkills(policy) {
|
|
118
160
|
const selected = []
|
|
119
161
|
const add = (id) => { if (id && !selected.includes(id)) selected.push(id) }
|
|
@@ -166,6 +208,218 @@ export default Plugin.define({
|
|
|
166
208
|
async setup(ctx) {
|
|
167
209
|
const projectRoot = ctx.location.project?.canonical || ctx.location.directory
|
|
168
210
|
const capabilities = runtimeCapabilities(ctx)
|
|
211
|
+
const runtimeGuard = createRuntimeGuard({ duplicateLimit: 3, loopLimit: 6 })
|
|
212
|
+
const sessionAssignments = new Map()
|
|
213
|
+
const eventController = new AbortController()
|
|
214
|
+
|
|
215
|
+
if (typeof ctx.event?.subscribe === "function") {
|
|
216
|
+
void (async () => {
|
|
217
|
+
try {
|
|
218
|
+
for await (const event of ctx.event.subscribe({ signal: eventController.signal })) {
|
|
219
|
+
if (!["message.part.updated", "message.part.delta", "message.updated"].includes(event?.type)) continue
|
|
220
|
+
const properties = event?.properties || {}
|
|
221
|
+
const sessionID =
|
|
222
|
+
properties.sessionID ||
|
|
223
|
+
properties.part?.sessionID ||
|
|
224
|
+
properties.info?.sessionID ||
|
|
225
|
+
null
|
|
226
|
+
if (sessionID && sessionAssignments.has(sessionID)) {
|
|
227
|
+
runtimeGuard.touch(sessionID)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch {}
|
|
231
|
+
})()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const leaseSupervisor = setInterval(() => {
|
|
235
|
+
const workRoot = path.join(projectRoot, ".ues-work")
|
|
236
|
+
if (!existsSync(workRoot)) return
|
|
237
|
+
let entries = []
|
|
238
|
+
try {
|
|
239
|
+
entries = readdirSync(workRoot, { withFileTypes: true })
|
|
240
|
+
} catch {
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
if (!entry.isDirectory() || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(entry.name)) continue
|
|
245
|
+
try {
|
|
246
|
+
runOcskill(["work", "recover", entry.name, projectRoot], projectRoot)
|
|
247
|
+
} catch {}
|
|
248
|
+
}
|
|
249
|
+
}, 60_000)
|
|
250
|
+
leaseSupervisor.unref?.()
|
|
251
|
+
|
|
252
|
+
async function waitForExecutorProgress(sessionID, options = {}) {
|
|
253
|
+
const timeoutMs = Math.max(30_000, Number(options.timeoutMs || 10 * 60_000))
|
|
254
|
+
const stallMs = Math.max(30_000, Math.min(Number(options.stallMs || 60_000), 5 * 60_000))
|
|
255
|
+
const startedAt = Date.now()
|
|
256
|
+
let lastContext = null
|
|
257
|
+
let polling = false
|
|
258
|
+
let rejectWatchdog = null
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const initial = await ctx.session.context({ sessionID })
|
|
262
|
+
lastContext = sessionContextDigest(initial)
|
|
263
|
+
} catch {}
|
|
264
|
+
runtimeGuard.touch(sessionID)
|
|
265
|
+
|
|
266
|
+
const watchdog = new Promise((_, reject) => {
|
|
267
|
+
rejectWatchdog = reject
|
|
268
|
+
})
|
|
269
|
+
const timer = setInterval(async () => {
|
|
270
|
+
if (polling) return
|
|
271
|
+
polling = true
|
|
272
|
+
try {
|
|
273
|
+
const now = Date.now()
|
|
274
|
+
if (now - startedAt >= timeoutMs) {
|
|
275
|
+
const error = new Error("UES executor timed out after " + timeoutMs + "ms")
|
|
276
|
+
error.code = "UES_TIMEOUT"
|
|
277
|
+
rejectWatchdog?.(error)
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const messages = await ctx.session.context({ sessionID })
|
|
283
|
+
const digest = sessionContextDigest(messages)
|
|
284
|
+
if (lastContext === null || digest !== lastContext) {
|
|
285
|
+
lastContext = digest
|
|
286
|
+
runtimeGuard.touch(sessionID, now)
|
|
287
|
+
}
|
|
288
|
+
} catch {}
|
|
289
|
+
|
|
290
|
+
const snapshot = runtimeGuard.snapshot(sessionID)
|
|
291
|
+
const watchdogState = progressWatchdogDecision(snapshot, now, stallMs)
|
|
292
|
+
if (watchdogState.stalled) {
|
|
293
|
+
const error = new Error("UES no-progress watchdog: executor stalled for " + watchdogState.idleMs + "ms")
|
|
294
|
+
error.code = "UES_STALLED"
|
|
295
|
+
rejectWatchdog?.(error)
|
|
296
|
+
}
|
|
297
|
+
} finally {
|
|
298
|
+
polling = false
|
|
299
|
+
}
|
|
300
|
+
}, 5_000)
|
|
301
|
+
timer.unref?.()
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
await Promise.race([
|
|
305
|
+
ctx.session.wait({ sessionID }),
|
|
306
|
+
watchdog,
|
|
307
|
+
])
|
|
308
|
+
} finally {
|
|
309
|
+
clearInterval(timer)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const assignment = sessionAssignments.get(sessionID)
|
|
313
|
+
if (assignment?.resumeRequired) {
|
|
314
|
+
const error = new Error("UES no-progress post-compaction resume: checkpoint.nextAction was not executed before session completion")
|
|
315
|
+
error.code = "UES_RESUME_STALLED"
|
|
316
|
+
throw error
|
|
317
|
+
}
|
|
318
|
+
return ctx.session.context({ sessionID })
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (typeof ctx.tool?.hook === "function") {
|
|
322
|
+
await ctx.tool.hook("execute.before", (event) => {
|
|
323
|
+
const assignment = sessionAssignments.get(event.sessionID)
|
|
324
|
+
if (!assignment) return
|
|
325
|
+
const signal = workspaceSignal(assignment.executionDir || projectRoot)
|
|
326
|
+
const decision = runtimeGuard.before({
|
|
327
|
+
sessionID: event.sessionID,
|
|
328
|
+
tool: event.tool,
|
|
329
|
+
input: event.input,
|
|
330
|
+
callID: event.callID,
|
|
331
|
+
cwd: assignment.executionDir || projectRoot,
|
|
332
|
+
workspaceSignal: signal,
|
|
333
|
+
})
|
|
334
|
+
if (decision.blocked) throw new Error(decision.message)
|
|
335
|
+
|
|
336
|
+
if (assignment.resumeRequired) {
|
|
337
|
+
try {
|
|
338
|
+
const args = [
|
|
339
|
+
"work", "checkpoint-resumed", assignment.slug, assignment.task, projectRoot,
|
|
340
|
+
"--run-id", assignment.runId,
|
|
341
|
+
"--reason", "tool-action-observed-after-compaction",
|
|
342
|
+
]
|
|
343
|
+
runOcskill(args, projectRoot)
|
|
344
|
+
} catch {}
|
|
345
|
+
assignment.resumeRequired = false
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
await ctx.tool.hook("execute.after", (event) => {
|
|
350
|
+
const assignment = sessionAssignments.get(event.sessionID)
|
|
351
|
+
if (!assignment) return
|
|
352
|
+
const signal = workspaceSignal(assignment.executionDir || projectRoot)
|
|
353
|
+
if (event.status === "completed") {
|
|
354
|
+
const shellInput = JSON.stringify(event.input || {})
|
|
355
|
+
const budgetTool =
|
|
356
|
+
event.tool === "bash" && /(?:ocskill\s+repo-graph|\brg\b|\bgrep\b|\bglob\b)/i.test(shellInput)
|
|
357
|
+
? "repo-graph"
|
|
358
|
+
: event.tool
|
|
359
|
+
event.result = budgetToolResult(budgetTool, event.result)
|
|
360
|
+
runtimeGuard.after({
|
|
361
|
+
sessionID: event.sessionID,
|
|
362
|
+
tool: event.tool,
|
|
363
|
+
input: event.input,
|
|
364
|
+
callID: event.callID,
|
|
365
|
+
cwd: assignment.executionDir || projectRoot,
|
|
366
|
+
workspaceSignal: signal,
|
|
367
|
+
status: "completed",
|
|
368
|
+
result: event.result,
|
|
369
|
+
})
|
|
370
|
+
} else {
|
|
371
|
+
runtimeGuard.after({
|
|
372
|
+
sessionID: event.sessionID,
|
|
373
|
+
tool: event.tool,
|
|
374
|
+
input: event.input,
|
|
375
|
+
callID: event.callID,
|
|
376
|
+
cwd: assignment.executionDir || projectRoot,
|
|
377
|
+
workspaceSignal: signal,
|
|
378
|
+
status: "error",
|
|
379
|
+
error: event.error,
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (capabilities.sessionHook) {
|
|
386
|
+
await ctx.session.hook("retry", (event) => {
|
|
387
|
+
const kind = classifyProviderFailure(event.error || {})
|
|
388
|
+
if (kind === "AUTH" || kind === "CONTEXT_TOO_LARGE") {
|
|
389
|
+
event.decision = { retry: false }
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
if (["RATE_LIMIT", "PROVIDER_5XX", "TIMEOUT", "NO_TOKEN"].includes(kind) && event.attempt < 3) {
|
|
393
|
+
event.decision = {
|
|
394
|
+
retry: true,
|
|
395
|
+
delay: kind === "RATE_LIMIT" ? Math.min(10_000, 1_000 * event.attempt) : 0,
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
await ctx.session.hook("compaction", (event) => {
|
|
401
|
+
const assignment = sessionAssignments.get(event.sessionID)
|
|
402
|
+
if (!assignment) return
|
|
403
|
+
try {
|
|
404
|
+
const checkpoint = runOcskillJSON([
|
|
405
|
+
"work", "checkpoint", assignment.slug, assignment.task, projectRoot,
|
|
406
|
+
"--run-id", assignment.runId,
|
|
407
|
+
"--reason", "pre-compaction",
|
|
408
|
+
], projectRoot)
|
|
409
|
+
assignment.checkpoint = checkpoint
|
|
410
|
+
assignment.resumeRequired = true
|
|
411
|
+
assignment.compactionAt = Date.now()
|
|
412
|
+
runtimeGuard.compacted(event.sessionID, assignment.compactionAt)
|
|
413
|
+
event.system.push({
|
|
414
|
+
type: "text",
|
|
415
|
+
text:
|
|
416
|
+
"UES durable checkpoint persisted before compaction. Preserve currentTaskId/runId/planHash/evidence pointers. " +
|
|
417
|
+
"After compaction, execute checkpoint.nextAction before explanatory prose. Checkpoint: " +
|
|
418
|
+
JSON.stringify(checkpoint),
|
|
419
|
+
})
|
|
420
|
+
} catch {}
|
|
421
|
+
})
|
|
422
|
+
}
|
|
169
423
|
|
|
170
424
|
await ctx.tool.transform((editor) => {
|
|
171
425
|
editor.namespace({
|
|
@@ -400,6 +654,7 @@ export default Plugin.define({
|
|
|
400
654
|
slug: { type: "string" },
|
|
401
655
|
task: { type: "string" },
|
|
402
656
|
timeoutMs: { type: "integer", minimum: 30000, maximum: 3600000 },
|
|
657
|
+
stallMs: { type: "integer", minimum: 30000, maximum: 300000 },
|
|
403
658
|
isolate: { type: "boolean" },
|
|
404
659
|
integrate: { type: "boolean" },
|
|
405
660
|
},
|
|
@@ -441,6 +696,10 @@ export default Plugin.define({
|
|
|
441
696
|
30_000,
|
|
442
697
|
Math.min(Number(input.timeoutMs || (taskPolicy.mode === "long-horizon" ? 20 * 60_000 : 10 * 60_000)), 60 * 60_000),
|
|
443
698
|
)
|
|
699
|
+
const stallMs = Math.max(
|
|
700
|
+
30_000,
|
|
701
|
+
Math.min(Number(input.stallMs || 60_000), 5 * 60_000),
|
|
702
|
+
)
|
|
444
703
|
const policyArgs = ["model-policy", "executor", "--attempt", String(attempt)]
|
|
445
704
|
if (taskText) policyArgs.push("--text", taskText)
|
|
446
705
|
const policy = runOcskillJSON(policyArgs, projectRoot)
|
|
@@ -477,57 +736,134 @@ export default Plugin.define({
|
|
|
477
736
|
}, 30_000)
|
|
478
737
|
|
|
479
738
|
try {
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}, projectRoot)
|
|
489
|
-
{
|
|
490
|
-
const attachArgs = [
|
|
491
|
-
"work", "attach-session", input.slug, input.task, projectRoot,
|
|
492
|
-
"--session-id", created.id,
|
|
493
|
-
]
|
|
494
|
-
if (runId) attachArgs.push("--run-id", runId)
|
|
495
|
-
attachArgs.push("--execution-dir", executionDir)
|
|
496
|
-
if (sandbox?.dir) attachArgs.push("--sandbox-dir", sandbox.dir)
|
|
497
|
-
runOcskill(attachArgs, projectRoot)
|
|
498
|
-
}
|
|
499
|
-
await ctx.session.switchAgent({ sessionID: created.id, agent: "ues-executor" })
|
|
500
|
-
const selectedModel = modelRef(policy?.model)
|
|
501
|
-
if (selectedModel) {
|
|
502
|
-
await ctx.session.switchModel({ sessionID: created.id, model: selectedModel })
|
|
503
|
-
}
|
|
739
|
+
const escalationArgs = ["model-policy", "executor", "--attempt", String(attempt + 1)]
|
|
740
|
+
if (taskText) escalationArgs.push("--text", taskText)
|
|
741
|
+
const escalatedPolicy = runOcskillJSON(escalationArgs, projectRoot)
|
|
742
|
+
let selectedPolicy = policy
|
|
743
|
+
let created = null
|
|
744
|
+
let messages = null
|
|
745
|
+
const providerRecovery = []
|
|
746
|
+
let physicalAttempt = 0
|
|
504
747
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
"
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
748
|
+
while (physicalAttempt < 3) {
|
|
749
|
+
physicalAttempt += 1
|
|
750
|
+
created = await ctx.session.create({
|
|
751
|
+
title: "UES " + input.slug + " " + input.task + " p" + physicalAttempt,
|
|
752
|
+
location: { directory: executionDir },
|
|
753
|
+
})
|
|
754
|
+
sessionAssignments.set(created.id, {
|
|
755
|
+
slug: input.slug,
|
|
756
|
+
task: input.task,
|
|
757
|
+
runId,
|
|
758
|
+
executionDir,
|
|
759
|
+
sandboxDir: sandbox?.dir || null,
|
|
760
|
+
resumeRequired: false,
|
|
761
|
+
checkpoint: null,
|
|
762
|
+
compactionAt: null,
|
|
763
|
+
})
|
|
764
|
+
runtimeGuard.touch(created.id)
|
|
765
|
+
|
|
766
|
+
appendTrace(traceID, "dispatch.session-created", {
|
|
767
|
+
sessionID: created.id,
|
|
768
|
+
executionDir,
|
|
769
|
+
isolated: executionDir !== projectRoot,
|
|
770
|
+
physicalAttempt,
|
|
771
|
+
model: selectedPolicy?.model || null,
|
|
772
|
+
}, projectRoot)
|
|
773
|
+
|
|
774
|
+
{
|
|
775
|
+
const attachArgs = [
|
|
776
|
+
"work", "attach-session", input.slug, input.task, projectRoot,
|
|
777
|
+
"--session-id", created.id,
|
|
778
|
+
]
|
|
779
|
+
if (runId) attachArgs.push("--run-id", runId)
|
|
780
|
+
attachArgs.push("--execution-dir", executionDir)
|
|
781
|
+
if (sandbox?.dir) attachArgs.push("--sandbox-dir", sandbox.dir)
|
|
782
|
+
runOcskill(attachArgs, projectRoot)
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
await ctx.session.switchAgent({ sessionID: created.id, agent: "ues-executor" })
|
|
786
|
+
const selectedModel = modelRef(selectedPolicy?.model)
|
|
787
|
+
if (selectedModel) {
|
|
788
|
+
await ctx.session.switchModel({ sessionID: created.id, model: selectedModel })
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const recovery = started?.contextPack?.contextPolicy?.recovery
|
|
792
|
+
const recoveryText = recovery?.requireDiagnosis
|
|
793
|
+
? " This is recovery attempt " + attempt + ". Diagnose the previous failure from fresh evidence before editing. " +
|
|
794
|
+
(recovery.directives || []).join("; ") + "."
|
|
795
|
+
: ""
|
|
796
|
+
const providerRecoveryText = providerRecovery.length
|
|
797
|
+
? " Provider/session recovery is active. Continue from durable .ues-work state and current workspace evidence; do not repeat already-proven exploration."
|
|
798
|
+
: ""
|
|
799
|
+
const dispatchContext = physicalAttempt === 1
|
|
800
|
+
? started.contextPack
|
|
801
|
+
: runOcskillJSON(["context-pack", input.slug, input.task, projectRoot], projectRoot)
|
|
802
|
+
|
|
803
|
+
await ctx.session.prompt({
|
|
804
|
+
sessionID: created.id,
|
|
805
|
+
text:
|
|
806
|
+
"Implement exactly this approved UES task in the current repository. " +
|
|
807
|
+
"Do not broaden scope or launch child agents. Run the declared verification and return the executor report." +
|
|
808
|
+
recoveryText + providerRecoveryText + "\n\n" +
|
|
809
|
+
JSON.stringify(dispatchContext, null, 2),
|
|
810
|
+
})
|
|
512
811
|
|
|
513
|
-
let timer = null
|
|
514
|
-
try {
|
|
515
|
-
await Promise.race([
|
|
516
|
-
ctx.session.wait({ sessionID: created.id }),
|
|
517
|
-
new Promise((_, reject) => {
|
|
518
|
-
timer = setTimeout(
|
|
519
|
-
() => reject(new Error("UES executor timed out after " + timeoutMs + "ms")),
|
|
520
|
-
timeoutMs,
|
|
521
|
-
)
|
|
522
|
-
}),
|
|
523
|
-
])
|
|
524
|
-
} catch (error) {
|
|
525
812
|
try {
|
|
526
|
-
await
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
813
|
+
messages = await waitForExecutorProgress(created.id, { timeoutMs, stallMs })
|
|
814
|
+
sessionAssignments.delete(created.id)
|
|
815
|
+
runtimeGuard.clear(created.id)
|
|
816
|
+
break
|
|
817
|
+
} catch (error) {
|
|
818
|
+
try {
|
|
819
|
+
await ctx.session.interrupt({ sessionID: created.id, continue: false })
|
|
820
|
+
} catch {}
|
|
821
|
+
|
|
822
|
+
const failureKind = classifyProviderFailure(error)
|
|
823
|
+
const escalationModel = modelRef(escalatedPolicy?.model)
|
|
824
|
+
const hasEscalationModel = Boolean(
|
|
825
|
+
escalationModel &&
|
|
826
|
+
escalatedPolicy?.model &&
|
|
827
|
+
escalatedPolicy.model !== selectedPolicy?.model,
|
|
828
|
+
)
|
|
829
|
+
const decision = providerRecoveryPlan(failureKind, physicalAttempt, {
|
|
830
|
+
hasEscalationModel,
|
|
831
|
+
})
|
|
832
|
+
providerRecovery.push({
|
|
833
|
+
physicalAttempt,
|
|
834
|
+
sessionID: created.id,
|
|
835
|
+
failureKind,
|
|
836
|
+
action: decision.action,
|
|
837
|
+
model: selectedPolicy?.model || null,
|
|
838
|
+
})
|
|
839
|
+
appendTrace(traceID, "dispatch.provider-recovery", {
|
|
840
|
+
task: input.task,
|
|
841
|
+
runId,
|
|
842
|
+
physicalAttempt,
|
|
843
|
+
sessionID: created.id,
|
|
844
|
+
failureKind,
|
|
845
|
+
decision,
|
|
846
|
+
model: selectedPolicy?.model || null,
|
|
847
|
+
escalationModel: escalatedPolicy?.model || null,
|
|
848
|
+
}, projectRoot)
|
|
849
|
+
|
|
850
|
+
sessionAssignments.delete(created.id)
|
|
851
|
+
runtimeGuard.clear(created.id)
|
|
852
|
+
|
|
853
|
+
if (!decision.retry || physicalAttempt >= 3) throw error
|
|
854
|
+
if (decision.action === "fresh-session-escalated-model") {
|
|
855
|
+
selectedPolicy = escalatedPolicy
|
|
856
|
+
}
|
|
857
|
+
await tool.progress({
|
|
858
|
+
status:
|
|
859
|
+
"provider recovery " + decision.action +
|
|
860
|
+
" after " + failureKind.toLowerCase().replaceAll("_", "-"),
|
|
861
|
+
})
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (!created || !messages) {
|
|
866
|
+
throw new Error("UES provider recovery exhausted without a completed executor session")
|
|
531
867
|
}
|
|
532
868
|
|
|
533
869
|
const afterWait = runOcskillJSON(["work", "status", input.slug, projectRoot], projectRoot)
|
|
@@ -538,12 +874,12 @@ export default Plugin.define({
|
|
|
538
874
|
throw new Error("UES executor attempt is no longer active; refusing post-cancel integration or completion handoff")
|
|
539
875
|
}
|
|
540
876
|
|
|
541
|
-
const messages = await ctx.session.context({ sessionID: created.id })
|
|
542
877
|
appendTrace(traceID, "dispatch.completed", {
|
|
543
878
|
sessionID: created.id,
|
|
544
879
|
task: input.task,
|
|
545
880
|
runId,
|
|
546
881
|
messageCount: Array.isArray(messages) ? messages.length : null,
|
|
882
|
+
providerRecovery,
|
|
547
883
|
}, projectRoot)
|
|
548
884
|
let integration = null
|
|
549
885
|
if (sandbox && input.integrate === true) {
|
|
@@ -561,12 +897,14 @@ export default Plugin.define({
|
|
|
561
897
|
runId,
|
|
562
898
|
traceID,
|
|
563
899
|
taskPolicy,
|
|
564
|
-
model:
|
|
900
|
+
model: selectedPolicy,
|
|
565
901
|
timeoutMs,
|
|
902
|
+
stallMs,
|
|
566
903
|
executionDir,
|
|
567
904
|
isolated: executionDir !== projectRoot,
|
|
568
905
|
sandbox,
|
|
569
906
|
integration,
|
|
907
|
+
providerRecovery,
|
|
570
908
|
messages: messageExcerpt(messages),
|
|
571
909
|
next: sandbox
|
|
572
910
|
? "Inspect and verify the isolated worktree first. If accepted, integrate it with ocskill sandbox integrate <worktree> . before recording work complete."
|
|
@@ -604,12 +942,21 @@ export default Plugin.define({
|
|
|
604
942
|
|
|
605
943
|
if (capabilities.sessionHook) {
|
|
606
944
|
await ctx.session.hook("context", (event) => {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
945
|
+
event.system.push({
|
|
946
|
+
type: "text",
|
|
947
|
+
text: "UES: use the minimum context that preserves correctness. FAST reads the target and nearest evidence with direct skills only; STANDARD/DEEP expand when risk or evidence requires it. Preserve exact contracts, verify fresh behavior, and escalate after failed attempts instead of stacking patches.",
|
|
948
|
+
})
|
|
949
|
+
const assignment = sessionAssignments.get(event.sessionID)
|
|
950
|
+
if (assignment?.resumeRequired && assignment.checkpoint) {
|
|
951
|
+
event.system.push({
|
|
952
|
+
type: "text",
|
|
953
|
+
text:
|
|
954
|
+
"UES post-compaction resume is mandatory. Before explanatory prose, execute this deterministic nextAction now: " +
|
|
955
|
+
JSON.stringify(assignment.checkpoint.nextAction) +
|
|
956
|
+
". Resume only from .ues-work state/evidence and the current workspace; do not replay external/destructive side effects.",
|
|
957
|
+
})
|
|
958
|
+
}
|
|
611
959
|
})
|
|
612
|
-
})
|
|
613
960
|
|
|
614
961
|
await ctx.session.hook("prompt", (event) => {
|
|
615
962
|
const config = routerConfig()
|
|
@@ -625,7 +972,7 @@ export default Plugin.define({
|
|
|
625
972
|
Math.min(config.maxSkills, Number(policy?.maxSkills || config.maxSkills)),
|
|
626
973
|
)
|
|
627
974
|
const selected = []
|
|
628
|
-
for (const id of [...
|
|
975
|
+
for (const id of [...routeSkillsForPolicy(event.prompt.text, policy, effectiveMaxSkills, routingFacts), ...policySkills(policy)]) {
|
|
629
976
|
if (!selected.includes(id)) selected.push(id)
|
|
630
977
|
if (selected.length >= effectiveMaxSkills) break
|
|
631
978
|
}
|
|
@@ -649,7 +996,7 @@ export default Plugin.define({
|
|
|
649
996
|
feedbackDomains: routingFacts.feedbackDomains,
|
|
650
997
|
acceptedLearningCount: routingFacts.acceptedLearningCount,
|
|
651
998
|
},
|
|
652
|
-
version:
|
|
999
|
+
version: 10,
|
|
653
1000
|
effectiveMaxSkills,
|
|
654
1001
|
},
|
|
655
1002
|
}
|
|
@@ -666,5 +1013,12 @@ export default Plugin.define({
|
|
|
666
1013
|
event.message = "UES safety gate: confirm destructive/high-impact shell action (" + risk.id + ")."
|
|
667
1014
|
})
|
|
668
1015
|
}
|
|
1016
|
+
|
|
1017
|
+
return () => {
|
|
1018
|
+
eventController.abort()
|
|
1019
|
+
clearInterval(leaseSupervisor)
|
|
1020
|
+
for (const sessionID of sessionAssignments.keys()) runtimeGuard.clear(sessionID)
|
|
1021
|
+
sessionAssignments.clear()
|
|
1022
|
+
}
|
|
669
1023
|
},
|
|
670
1024
|
})
|
|
@@ -189,3 +189,38 @@ export function routeSkills(text, maxSkills = 4, facts = {}) {
|
|
|
189
189
|
|
|
190
190
|
return routed.slice(0, limit)
|
|
191
191
|
}
|
|
192
|
+
|
|
193
|
+
export function routeSkillsForPolicy(text, policy = {}, maxSkills = 4, facts = {}) {
|
|
194
|
+
const limit = Number.isInteger(maxSkills) ? Math.max(1, Math.min(maxSkills, 6)) : 4
|
|
195
|
+
const routed = routeSkills(text, 6, facts)
|
|
196
|
+
const fast =
|
|
197
|
+
policy?.executionProfile === "fast" &&
|
|
198
|
+
policy?.risk !== "high" &&
|
|
199
|
+
policy?.mode !== "long-horizon"
|
|
200
|
+
|
|
201
|
+
if (!fast) return routed.slice(0, limit)
|
|
202
|
+
|
|
203
|
+
const eligible = routed.filter((id) =>
|
|
204
|
+
![
|
|
205
|
+
"ues-engineering-orchestrator",
|
|
206
|
+
"ues-change-impact-analysis",
|
|
207
|
+
"ues-task-planner",
|
|
208
|
+
"ues-long-task-state",
|
|
209
|
+
].includes(id),
|
|
210
|
+
)
|
|
211
|
+
const selected = [
|
|
212
|
+
...eligible.filter((id) => !PROCESS_SKILLS.has(id)),
|
|
213
|
+
...eligible.filter((id) => PROCESS_SKILLS.has(id)),
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
const value = String(text || "").toLowerCase()
|
|
217
|
+
if (/(review|audit|kiểm tra code|đánh giá)/.test(value)) add(selected, "ues-code-review")
|
|
218
|
+
if (/(verify|verification|test|tests|kiểm thử|xác minh)/.test(value)) add(selected, "ues-test-verification")
|
|
219
|
+
|
|
220
|
+
if (selected.length === 0 && /(code|repository|repo|project|function|class|endpoint|hàm|lớp|dự án)/.test(value)) {
|
|
221
|
+
add(selected, "ues-repo-explorer")
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return selected.slice(0, limit)
|
|
225
|
+
}
|
|
226
|
+
|