@bojackduy/opencode-loopd 1.9.7 → 1.10.1
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/server.js +355 -50
- package/dist/tui.js +705 -490
- package/package.json +2 -1
package/dist/server.js
CHANGED
|
@@ -487,7 +487,7 @@ var CURRENT_VERSION = 6, LOCK_STALE_MS = 1e4;
|
|
|
487
487
|
var init_state_repository = () => {};
|
|
488
488
|
|
|
489
489
|
// src/server/plugin.ts
|
|
490
|
-
import {
|
|
490
|
+
import { tool as v1Tool } from "@opencode-ai/plugin/tool";
|
|
491
491
|
|
|
492
492
|
// src/application/control-worker.ts
|
|
493
493
|
init_state_repository();
|
|
@@ -1639,6 +1639,9 @@ function createLoopEngine(options) {
|
|
|
1639
1639
|
}
|
|
1640
1640
|
return true;
|
|
1641
1641
|
}
|
|
1642
|
+
function hasExecutionLeak(runtime) {
|
|
1643
|
+
return runtime.phase !== "idle" || Boolean(runtime.activeRunID) || Boolean(runtime.leaseExpiresAt) || Boolean(runtime.turnStartedAt) || Boolean(runtime.activePromptMessageID) || Boolean(runtime.activePromptObservedAt) || Boolean(runtime.activeAssistantMessageID) || Boolean(runtime.activeAssistantCompletedAt) || Boolean(runtime.idleCandidateAt) || (runtime.activeToolCallIDs?.length ?? 0) > 0;
|
|
1644
|
+
}
|
|
1642
1645
|
async function maintenance() {
|
|
1643
1646
|
syncWorkerSessionsFromService();
|
|
1644
1647
|
if (knownWorkerSessions.size === 0)
|
|
@@ -1650,10 +1653,11 @@ function createLoopEngine(options) {
|
|
|
1650
1653
|
const rt = state.runtimes.find((r) => r.goalID === goal.id);
|
|
1651
1654
|
if (!rt)
|
|
1652
1655
|
continue;
|
|
1653
|
-
if (
|
|
1656
|
+
if (hasExecutionLeak(rt)) {
|
|
1654
1657
|
await mutateState(directory, `maintenance.clear-terminal-leak:${goal.id}`, async (s) => {
|
|
1655
1658
|
const r = s.runtimes.find((x) => x.goalID === goal.id);
|
|
1656
|
-
|
|
1659
|
+
const g = s.goals.find((item) => item.id === goal.id);
|
|
1660
|
+
if (g?.status !== "active" && r && hasExecutionLeak(r)) {
|
|
1657
1661
|
Object.assign(r, releaseLease(r));
|
|
1658
1662
|
r.activeRunID = undefined;
|
|
1659
1663
|
r.unknownStatusCount = 0;
|
|
@@ -2103,6 +2107,115 @@ function createRealHost(client, directory) {
|
|
|
2103
2107
|
}
|
|
2104
2108
|
};
|
|
2105
2109
|
}
|
|
2110
|
+
function createV2Host(context, statuses) {
|
|
2111
|
+
const directory = context.location.directory;
|
|
2112
|
+
return {
|
|
2113
|
+
async createWorker({ parentID, title, agent, model }) {
|
|
2114
|
+
const session = await context.session.create({
|
|
2115
|
+
title,
|
|
2116
|
+
agent,
|
|
2117
|
+
model: model ? { id: model.modelID, providerID: model.providerID } : undefined,
|
|
2118
|
+
location: { directory },
|
|
2119
|
+
metadata: { "loopd.parentID": parentID }
|
|
2120
|
+
});
|
|
2121
|
+
statuses.set(session.id, "idle");
|
|
2122
|
+
await logServerEvent(directory, "worker.created", { parentID, workerSessionID: session.id, title });
|
|
2123
|
+
return session.id;
|
|
2124
|
+
},
|
|
2125
|
+
async promptWorker({ sessionID, prompt, messageID, model, agent }) {
|
|
2126
|
+
if (agent)
|
|
2127
|
+
await context.session.switchAgent({ sessionID, agent });
|
|
2128
|
+
if (model) {
|
|
2129
|
+
await context.session.switchModel({
|
|
2130
|
+
sessionID,
|
|
2131
|
+
model: { id: model.modelID, providerID: model.providerID }
|
|
2132
|
+
});
|
|
2133
|
+
}
|
|
2134
|
+
const result = await context.session.prompt({
|
|
2135
|
+
sessionID,
|
|
2136
|
+
id: messageID,
|
|
2137
|
+
text: prompt
|
|
2138
|
+
});
|
|
2139
|
+
statuses.set(sessionID, "busy");
|
|
2140
|
+
await logServerEvent(directory, "worker.prompted", { sessionID });
|
|
2141
|
+
return { messageID: result.id };
|
|
2142
|
+
},
|
|
2143
|
+
async readSession(sessionID) {
|
|
2144
|
+
try {
|
|
2145
|
+
const session = await context.session.get({ sessionID });
|
|
2146
|
+
const model = session.model ? { providerID: session.model.providerID, modelID: session.model.id } : undefined;
|
|
2147
|
+
if (!session.agent && !model)
|
|
2148
|
+
return;
|
|
2149
|
+
return { agent: session.agent, model };
|
|
2150
|
+
} catch {
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
},
|
|
2154
|
+
async sessionStatus(sessionID) {
|
|
2155
|
+
return statuses.get(sessionID) ?? "unknown";
|
|
2156
|
+
},
|
|
2157
|
+
async abortSession(sessionID) {
|
|
2158
|
+
try {
|
|
2159
|
+
await context.session.interrupt({ sessionID });
|
|
2160
|
+
statuses.set(sessionID, "idle");
|
|
2161
|
+
} catch {}
|
|
2162
|
+
},
|
|
2163
|
+
async readMessages(sessionID, limit = 10) {
|
|
2164
|
+
try {
|
|
2165
|
+
const messages = await context.session.context({ sessionID });
|
|
2166
|
+
let parentMessageID;
|
|
2167
|
+
return messages.flatMap((message) => {
|
|
2168
|
+
if (message.type === "user") {
|
|
2169
|
+
parentMessageID = message.id;
|
|
2170
|
+
return [{
|
|
2171
|
+
role: "user",
|
|
2172
|
+
content: message.text,
|
|
2173
|
+
timestamp: new Date(message.time.created).toISOString(),
|
|
2174
|
+
messageID: message.id
|
|
2175
|
+
}];
|
|
2176
|
+
}
|
|
2177
|
+
if (message.type !== "assistant")
|
|
2178
|
+
return [];
|
|
2179
|
+
const created = message.time.created;
|
|
2180
|
+
const completed = message.time.completed;
|
|
2181
|
+
return [{
|
|
2182
|
+
role: "assistant",
|
|
2183
|
+
content: message.content.filter((part) => part.type === "text").map((part) => part.text).join(`
|
|
2184
|
+
`),
|
|
2185
|
+
timestamp: new Date(completed ?? created).toISOString(),
|
|
2186
|
+
messageID: message.id,
|
|
2187
|
+
parentMessageID,
|
|
2188
|
+
completedAt: completed ? new Date(completed).toISOString() : undefined,
|
|
2189
|
+
tokens: message.tokens ? {
|
|
2190
|
+
input: message.tokens.input,
|
|
2191
|
+
output: message.tokens.output,
|
|
2192
|
+
reasoning: message.tokens.reasoning,
|
|
2193
|
+
cacheRead: message.tokens.cache.read,
|
|
2194
|
+
cacheWrite: message.tokens.cache.write
|
|
2195
|
+
} : undefined,
|
|
2196
|
+
cost: message.cost,
|
|
2197
|
+
durationMs: completed && completed >= created ? completed - created : undefined
|
|
2198
|
+
}];
|
|
2199
|
+
}).slice(-limit);
|
|
2200
|
+
} catch {
|
|
2201
|
+
return [];
|
|
2202
|
+
}
|
|
2203
|
+
},
|
|
2204
|
+
async compactSession(sessionID) {
|
|
2205
|
+
try {
|
|
2206
|
+
await context.session.command({ sessionID, name: "compact", text: "" });
|
|
2207
|
+
} catch {}
|
|
2208
|
+
},
|
|
2209
|
+
async notifyOwner(ownerSessionID, message) {
|
|
2210
|
+
try {
|
|
2211
|
+
await context.session.prompt({ sessionID: ownerSessionID, text: message });
|
|
2212
|
+
await logServerEvent(directory, "parent.notified", { ownerSessionID, preview: message.slice(0, 160) });
|
|
2213
|
+
} catch (error) {
|
|
2214
|
+
await logServerEvent(directory, "parent.notify.failed", { ownerSessionID, detail: describeError(error) });
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2106
2219
|
async function withTimeout(promise, timeoutMs, operation) {
|
|
2107
2220
|
let timer;
|
|
2108
2221
|
try {
|
|
@@ -2785,9 +2898,13 @@ function createGoalService(host) {
|
|
|
2785
2898
|
runtime.consecutiveFailures = 0;
|
|
2786
2899
|
runtime.lastError = undefined;
|
|
2787
2900
|
runtime.forceFinishRequested = undefined;
|
|
2901
|
+
runtime.evaluatorRejectionCount = 0;
|
|
2902
|
+
runtime.lastRejectionDetails = undefined;
|
|
2903
|
+
runtime.freeRetryPending = false;
|
|
2788
2904
|
runtime.lastParentNotifiedAt = undefined;
|
|
2789
2905
|
runtime.lastParentNotifiedFor = undefined;
|
|
2790
|
-
runtime
|
|
2906
|
+
Object.assign(runtime, releaseLease(runtime));
|
|
2907
|
+
runtime.activeRunID = undefined;
|
|
2791
2908
|
runtime.updatedAt = new Date().toISOString();
|
|
2792
2909
|
}
|
|
2793
2910
|
return state;
|
|
@@ -3419,25 +3536,32 @@ function goalTools(dir, goalService, hostSessionID, defaults = {}, host) {
|
|
|
3419
3536
|
const cwd = goal.config.checkCwd || goal.config.artifactDir || dir;
|
|
3420
3537
|
const checkResults = await runCompletionChecks(goal.config.checks, cwd);
|
|
3421
3538
|
if (!checkResults.passed) {
|
|
3422
|
-
const
|
|
3423
|
-
|
|
3424
|
-
runtime.evaluatorRejectionCount = (runtime.evaluatorRejectionCount || 0) + 1;
|
|
3425
|
-
const failureDetails = checkResults.failures.map((f) => {
|
|
3426
|
-
const stdoutSnippet = f.stdout ? `
|
|
3539
|
+
const failureDetails = checkResults.failures.map((f) => {
|
|
3540
|
+
const stdoutSnippet = f.stdout ? `
|
|
3427
3541
|
Stdout: ${f.stdout.slice(0, 500)}` : "";
|
|
3428
|
-
|
|
3542
|
+
const stderrSnippet = f.stderr ? `
|
|
3429
3543
|
Stderr: ${f.stderr.slice(0, 500)}` : "";
|
|
3430
|
-
|
|
3544
|
+
return `Command: ${f.command}
|
|
3431
3545
|
Exit code: ${f.exitCode}${stdoutSnippet}${stderrSnippet}`;
|
|
3432
|
-
|
|
3546
|
+
}).join(`
|
|
3433
3547
|
|
|
3434
3548
|
`);
|
|
3549
|
+
let rejectionCount = 0;
|
|
3550
|
+
let blocked = false;
|
|
3551
|
+
let attemptID = "";
|
|
3552
|
+
const rejectionState = await mutateState(dir, `goal.completion-rejected:${goal.id}`, async (current) => {
|
|
3553
|
+
const currentGoal = current.goals.find((item) => item.id === goal.id);
|
|
3554
|
+
const runtime = current.runtimes.find((r) => r.goalID === goal.id);
|
|
3555
|
+
if (!currentGoal || !runtime || currentGoal.status !== "active")
|
|
3556
|
+
return current;
|
|
3557
|
+
runtime.evaluatorRejectionCount = (runtime.evaluatorRejectionCount || 0) + 1;
|
|
3558
|
+
rejectionCount = runtime.evaluatorRejectionCount;
|
|
3435
3559
|
runtime.lastRejectionDetails = `Rejection #${runtime.evaluatorRejectionCount} at ${new Date().toISOString()}
|
|
3436
3560
|
|
|
3437
3561
|
Working directory: ${cwd}
|
|
3438
3562
|
|
|
3439
3563
|
${failureDetails}`;
|
|
3440
|
-
|
|
3564
|
+
attemptID = randomUUID5();
|
|
3441
3565
|
const verificationAttempt = {
|
|
3442
3566
|
id: attemptID,
|
|
3443
3567
|
sequence: runtime.evaluatorRejectionCount,
|
|
@@ -3457,55 +3581,71 @@ ${failureDetails}`;
|
|
|
3457
3581
|
};
|
|
3458
3582
|
runtime.lastVerificationAttempt = verificationAttempt;
|
|
3459
3583
|
runtime.recentVerificationAttempts = appendVerificationAttempt(runtime.recentVerificationAttempts || [], verificationAttempt);
|
|
3460
|
-
const
|
|
3461
|
-
version: 1,
|
|
3462
|
-
eventID: randomUUID5(),
|
|
3463
|
-
goalID: goal.id,
|
|
3464
|
-
type: "goal.completion_rejected",
|
|
3465
|
-
attemptID,
|
|
3466
|
-
rejectionCount: runtime.evaluatorRejectionCount,
|
|
3467
|
-
failedCheckCount: checkResults.failures.length,
|
|
3468
|
-
failureSummary: failureDetails.slice(0, 500),
|
|
3469
|
-
timestamp: new Date().toISOString(),
|
|
3470
|
-
revision: state.revision
|
|
3471
|
-
};
|
|
3472
|
-
await appendEvent(dir, rejectEvent);
|
|
3473
|
-
const maxRejections = goal.config.maxEvaluatorRejections || 3;
|
|
3584
|
+
const maxRejections = currentGoal.config.maxEvaluatorRejections || 3;
|
|
3474
3585
|
if (runtime.evaluatorRejectionCount >= maxRejections) {
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3586
|
+
blocked = true;
|
|
3587
|
+
currentGoal.status = "blocked";
|
|
3588
|
+
currentGoal.updatedAt = new Date().toISOString();
|
|
3589
|
+
currentGoal.blocker = {
|
|
3478
3590
|
reason: `Evaluator rejected ${runtime.evaluatorRejectionCount} time(s). Last failure:
|
|
3479
3591
|
${failureDetails.slice(0, 500)}`,
|
|
3480
3592
|
needed: "Fix the failing checks and retry the goal.",
|
|
3481
3593
|
at: new Date().toISOString()
|
|
3482
3594
|
};
|
|
3595
|
+
Object.assign(runtime, releaseLease(runtime));
|
|
3596
|
+
runtime.activeRunID = undefined;
|
|
3483
3597
|
runtime.forceFinishRequested = undefined;
|
|
3484
|
-
|
|
3485
|
-
version: 1,
|
|
3486
|
-
eventID: randomUUID5(),
|
|
3487
|
-
goalID: goal.id,
|
|
3488
|
-
type: "goal.blocked",
|
|
3489
|
-
reason: goal.blocker.reason,
|
|
3490
|
-
needed: goal.blocker.needed,
|
|
3491
|
-
timestamp: new Date().toISOString(),
|
|
3492
|
-
revision: state.revision
|
|
3493
|
-
});
|
|
3598
|
+
runtime.freeRetryPending = false;
|
|
3494
3599
|
} else {
|
|
3495
3600
|
runtime.forceFinishRequested = false;
|
|
3496
3601
|
runtime.freeRetryPending = true;
|
|
3497
3602
|
}
|
|
3498
3603
|
runtime.updatedAt = new Date().toISOString();
|
|
3499
|
-
|
|
3604
|
+
return current;
|
|
3605
|
+
});
|
|
3606
|
+
if (attemptID) {
|
|
3607
|
+
await appendEvent(dir, {
|
|
3608
|
+
version: 1,
|
|
3609
|
+
eventID: randomUUID5(),
|
|
3610
|
+
goalID: goal.id,
|
|
3611
|
+
type: "goal.completion_rejected",
|
|
3612
|
+
attemptID,
|
|
3613
|
+
rejectionCount,
|
|
3614
|
+
failedCheckCount: checkResults.failures.length,
|
|
3615
|
+
failureSummary: failureDetails.slice(0, 500),
|
|
3616
|
+
timestamp: new Date().toISOString(),
|
|
3617
|
+
revision: rejectionState.revision
|
|
3618
|
+
});
|
|
3619
|
+
}
|
|
3620
|
+
const rejectedGoal = rejectionState.goals.find((item) => item.id === goal.id);
|
|
3621
|
+
const rejectedRuntime = rejectionState.runtimes.find((item) => item.goalID === goal.id);
|
|
3622
|
+
rejectionCount = rejectedRuntime?.evaluatorRejectionCount ?? rejectionCount;
|
|
3623
|
+
const goalIsBlocked = rejectedGoal?.status === "blocked";
|
|
3624
|
+
if (blocked && rejectedGoal?.blocker) {
|
|
3625
|
+
await appendEvent(dir, {
|
|
3626
|
+
version: 1,
|
|
3627
|
+
eventID: randomUUID5(),
|
|
3628
|
+
goalID: goal.id,
|
|
3629
|
+
type: "goal.blocked",
|
|
3630
|
+
reason: rejectedGoal.blocker.reason,
|
|
3631
|
+
needed: rejectedGoal.blocker.needed,
|
|
3632
|
+
timestamp: new Date().toISOString(),
|
|
3633
|
+
revision: rejectionState.revision
|
|
3634
|
+
});
|
|
3635
|
+
await goalService.accountUsage(dir, goal.id).catch(() => {
|
|
3636
|
+
return;
|
|
3637
|
+
});
|
|
3500
3638
|
}
|
|
3501
3639
|
return {
|
|
3502
|
-
title: "Completion rejected \u2014 keep working",
|
|
3640
|
+
title: goalIsBlocked ? "Completion rejected \u2014 goal blocked" : "Completion rejected \u2014 keep working",
|
|
3503
3641
|
output: JSON.stringify({
|
|
3642
|
+
goalID: goal.id,
|
|
3643
|
+
goalName: goal.name,
|
|
3504
3644
|
passed: false,
|
|
3505
3645
|
failedChecks: checkResults.failures,
|
|
3506
|
-
message: "Evaluator rejected completion. Fix the issues above and try again.",
|
|
3507
|
-
rejectionCount
|
|
3508
|
-
status: goal.status
|
|
3646
|
+
message: goalIsBlocked ? "Evaluator rejection limit reached. Goal blocked; owner retry required." : "Evaluator rejected completion. Fix the issues above and try again.",
|
|
3647
|
+
rejectionCount,
|
|
3648
|
+
status: rejectedGoal?.status ?? goal.status
|
|
3509
3649
|
})
|
|
3510
3650
|
};
|
|
3511
3651
|
}
|
|
@@ -3772,6 +3912,42 @@ async function runCompletionChecks(checks, cwd) {
|
|
|
3772
3912
|
// src/server/owner-tools.ts
|
|
3773
3913
|
init_state_repository();
|
|
3774
3914
|
import { tool as tool2 } from "@opencode-ai/plugin/tool";
|
|
3915
|
+
|
|
3916
|
+
// src/domain/status-labels.ts
|
|
3917
|
+
var GOAL_STATUS_META = {
|
|
3918
|
+
active: { short: "Active", hint: "loopd owns it" },
|
|
3919
|
+
paused: { short: "Paused", hint: "stopped by you" },
|
|
3920
|
+
blocked: { short: "Blocked", hint: "needs you" },
|
|
3921
|
+
budget_limited: { short: "Out of budget", hint: "resume to spend" },
|
|
3922
|
+
usage_limited: { short: "Waiting for capacity", hint: "auto-resumes" },
|
|
3923
|
+
complete: { short: "Done", hint: "verified" }
|
|
3924
|
+
};
|
|
3925
|
+
var PHASE_META = {
|
|
3926
|
+
idle: { short: "Idle", hint: "between turns" },
|
|
3927
|
+
queued: { short: "Queued", hint: "waiting to start" },
|
|
3928
|
+
running: { short: "Running", hint: "worker acting now" },
|
|
3929
|
+
compacting: { short: "Compacting", hint: "summarizing context" },
|
|
3930
|
+
waiting_retry: { short: "Retrying", hint: "backing off" },
|
|
3931
|
+
stopping: { short: "Stopping", hint: "abort in flight" }
|
|
3932
|
+
};
|
|
3933
|
+
function goalStatusLabel(status) {
|
|
3934
|
+
return GOAL_STATUS_META[status] ?? { short: status, hint: "" };
|
|
3935
|
+
}
|
|
3936
|
+
function phaseLabel(phase) {
|
|
3937
|
+
return PHASE_META[phase] ?? { short: phase, hint: "" };
|
|
3938
|
+
}
|
|
3939
|
+
function describeGoalState(status, phase) {
|
|
3940
|
+
const goal = goalStatusLabel(status);
|
|
3941
|
+
const activity = phase ? phaseLabel(phase) : undefined;
|
|
3942
|
+
const workerClause = activity ? `worker is ${activity.hint || activity.short.toLowerCase()}` : "worker state unknown";
|
|
3943
|
+
if (status === "active")
|
|
3944
|
+
return `Loopd owns this; ${workerClause}.`;
|
|
3945
|
+
if (status === "complete")
|
|
3946
|
+
return "Done \u2014 verified; worker is stopped.";
|
|
3947
|
+
return `Parked \u2014 ${goal.hint || goal.short.toLowerCase()}; ${workerClause}.`;
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
// src/server/owner-tools.ts
|
|
3775
3951
|
import { promises as fs3 } from "fs";
|
|
3776
3952
|
function withTimeout2(promise, ms) {
|
|
3777
3953
|
return Promise.race([
|
|
@@ -3807,11 +3983,17 @@ function ownerTools(options) {
|
|
|
3807
3983
|
}
|
|
3808
3984
|
const summaries = goals.map((g) => {
|
|
3809
3985
|
const runtime = state.runtimes.find((r) => r.goalID === g.id);
|
|
3986
|
+
const phase = runtime?.phase ?? "unknown";
|
|
3987
|
+
const goalLabel = goalStatusLabel(g.status);
|
|
3988
|
+
const activityLabel = phaseLabel(phase);
|
|
3810
3989
|
return {
|
|
3811
3990
|
id: g.id,
|
|
3812
3991
|
name: g.name,
|
|
3813
3992
|
status: g.status,
|
|
3814
|
-
|
|
3993
|
+
statusDisplay: `${goalLabel.short} \u2014 ${goalLabel.hint}`,
|
|
3994
|
+
phase,
|
|
3995
|
+
activityDisplay: `${activityLabel.short} \u2014 ${activityLabel.hint}`,
|
|
3996
|
+
stateSummary: describeGoalState(g.status, runtime?.phase),
|
|
3815
3997
|
turn: runtime?.runCount ?? 0,
|
|
3816
3998
|
budgetTurnCount: runtime?.budgetTurnCount ?? 0,
|
|
3817
3999
|
maxTurns: g.config.maxTurns,
|
|
@@ -3881,6 +4063,9 @@ function ownerTools(options) {
|
|
|
3881
4063
|
name: goal.name,
|
|
3882
4064
|
objective: goal.objective,
|
|
3883
4065
|
status: goal.status,
|
|
4066
|
+
statusDisplay: `${goalStatusLabel(goal.status).short} \u2014 ${goalStatusLabel(goal.status).hint}`,
|
|
4067
|
+
activityDisplay: runtime?.phase ? `${phaseLabel(runtime.phase).short} \u2014 ${phaseLabel(runtime.phase).hint}` : undefined,
|
|
4068
|
+
stateSummary: describeGoalState(goal.status, runtime?.phase),
|
|
3884
4069
|
ownerSessionID: goal.ownerSessionID,
|
|
3885
4070
|
workerSessionID: goal.workerSessionID,
|
|
3886
4071
|
config: {
|
|
@@ -4337,6 +4522,9 @@ var PLUGIN_ID = "opencode-loopd.server";
|
|
|
4337
4522
|
var server = async ({ client, directory }, pluginOptions) => {
|
|
4338
4523
|
const defaults = parsePluginDefaults(pluginOptions);
|
|
4339
4524
|
const host = createRealHost(client, directory);
|
|
4525
|
+
return createServerHooks(directory, host, defaults);
|
|
4526
|
+
};
|
|
4527
|
+
function createServerHooks(directory, host, defaults) {
|
|
4340
4528
|
const goalService = createGoalService(host);
|
|
4341
4529
|
const worker = createControlWorker({
|
|
4342
4530
|
directory,
|
|
@@ -4465,7 +4653,7 @@ var server = async ({ client, directory }, pluginOptions) => {
|
|
|
4465
4653
|
scheduleWorker.stop();
|
|
4466
4654
|
}
|
|
4467
4655
|
};
|
|
4468
|
-
}
|
|
4656
|
+
}
|
|
4469
4657
|
function parsePluginDefaults(options) {
|
|
4470
4658
|
const agent = typeof options?.defaultAgent === "string" ? options.defaultAgent.trim() : "";
|
|
4471
4659
|
const model = typeof options?.defaultModel === "string" ? options.defaultModel.trim() : "";
|
|
@@ -4476,10 +4664,127 @@ function parsePluginDefaults(options) {
|
|
|
4476
4664
|
defaultChecks: checks.length > 0 ? checks : undefined
|
|
4477
4665
|
};
|
|
4478
4666
|
}
|
|
4479
|
-
var v2 =
|
|
4667
|
+
var v2 = {
|
|
4480
4668
|
id: PLUGIN_ID,
|
|
4481
|
-
async setup() {
|
|
4482
|
-
|
|
4669
|
+
async setup(context) {
|
|
4670
|
+
const directory = context.location.directory;
|
|
4671
|
+
const statuses = new Map;
|
|
4672
|
+
const host = createV2Host(context, statuses);
|
|
4673
|
+
const hooks = createServerHooks(directory, host, parsePluginDefaults(context.options));
|
|
4674
|
+
const registrations = [];
|
|
4675
|
+
const eventController = new AbortController;
|
|
4676
|
+
let eventTask = Promise.resolve();
|
|
4677
|
+
let disposed = false;
|
|
4678
|
+
const cleanup = async () => {
|
|
4679
|
+
if (disposed)
|
|
4680
|
+
return;
|
|
4681
|
+
disposed = true;
|
|
4682
|
+
eventController.abort();
|
|
4683
|
+
await eventTask;
|
|
4684
|
+
try {
|
|
4685
|
+
await Promise.allSettled(registrations.reverse().map((registration) => registration.dispose()));
|
|
4686
|
+
} finally {
|
|
4687
|
+
await hooks.dispose?.();
|
|
4688
|
+
}
|
|
4689
|
+
};
|
|
4690
|
+
try {
|
|
4691
|
+
registrations.push(await context.tool.transform((editor) => {
|
|
4692
|
+
for (const [id, definition] of Object.entries(hooks.tool ?? {})) {
|
|
4693
|
+
editor.add(toV2Tool(id, definition, directory));
|
|
4694
|
+
}
|
|
4695
|
+
}));
|
|
4696
|
+
registrations.push(await context.tool.hook("execute.before", async (input) => {
|
|
4697
|
+
await hooks["tool.execute.before"]?.({
|
|
4698
|
+
tool: input.tool,
|
|
4699
|
+
sessionID: input.sessionID,
|
|
4700
|
+
callID: input.id
|
|
4701
|
+
}, { args: input.input });
|
|
4702
|
+
}));
|
|
4703
|
+
registrations.push(await context.tool.hook("execute.after", async (input) => {
|
|
4704
|
+
const output = input.status === "completed" ? input.result : { content: JSON.stringify(input.error) };
|
|
4705
|
+
await hooks["tool.execute.after"]?.({
|
|
4706
|
+
tool: input.tool,
|
|
4707
|
+
sessionID: input.sessionID,
|
|
4708
|
+
callID: input.id,
|
|
4709
|
+
args: input.input
|
|
4710
|
+
}, {
|
|
4711
|
+
title: "",
|
|
4712
|
+
output: typeof output.content === "string" ? output.content : JSON.stringify(output.content ?? ""),
|
|
4713
|
+
metadata: output.metadata ?? {}
|
|
4714
|
+
});
|
|
4715
|
+
}));
|
|
4716
|
+
eventTask = consumeV2Events(context, eventController.signal, statuses, hooks).catch(async (error) => {
|
|
4717
|
+
if (!eventController.signal.aborted) {
|
|
4718
|
+
await logServerEvent(directory, "events.failed", { detail: describeError(error) });
|
|
4719
|
+
}
|
|
4720
|
+
});
|
|
4721
|
+
return cleanup;
|
|
4722
|
+
} catch (error) {
|
|
4723
|
+
await cleanup();
|
|
4724
|
+
throw error;
|
|
4725
|
+
}
|
|
4726
|
+
}
|
|
4727
|
+
};
|
|
4728
|
+
function toV2Tool(id, definition, directory) {
|
|
4729
|
+
return {
|
|
4730
|
+
name: id,
|
|
4731
|
+
description: definition.description,
|
|
4732
|
+
input: v1Tool.schema.object(definition.args),
|
|
4733
|
+
async execute(input, context) {
|
|
4734
|
+
const result = await definition.execute(input, {
|
|
4735
|
+
sessionID: context.sessionID,
|
|
4736
|
+
agent: context.agent,
|
|
4737
|
+
messageID: context.messageID,
|
|
4738
|
+
directory,
|
|
4739
|
+
worktree: directory,
|
|
4740
|
+
abort: new AbortController().signal,
|
|
4741
|
+
metadata() {},
|
|
4742
|
+
async ask() {}
|
|
4743
|
+
});
|
|
4744
|
+
if (typeof result === "string")
|
|
4745
|
+
return { content: result };
|
|
4746
|
+
return {
|
|
4747
|
+
content: result.output,
|
|
4748
|
+
metadata: {
|
|
4749
|
+
...result.metadata,
|
|
4750
|
+
...result.title ? { title: result.title } : {}
|
|
4751
|
+
}
|
|
4752
|
+
};
|
|
4753
|
+
}
|
|
4754
|
+
};
|
|
4755
|
+
}
|
|
4756
|
+
async function consumeV2Events(context, signal, statuses, hooks) {
|
|
4757
|
+
for await (const event of context.event.subscribe({ signal })) {
|
|
4758
|
+
const data = "data" in event && event.data && typeof event.data === "object" ? event.data : {};
|
|
4759
|
+
const sessionID = typeof data.sessionID === "string" ? data.sessionID : undefined;
|
|
4760
|
+
if (sessionID) {
|
|
4761
|
+
if (event.type === "session.status")
|
|
4762
|
+
statuses.set(sessionID, data.status?.type ?? "unknown");
|
|
4763
|
+
else if (event.type === "session.idle")
|
|
4764
|
+
statuses.set(sessionID, "idle");
|
|
4765
|
+
else if (event.type === "session.execution.started")
|
|
4766
|
+
statuses.set(sessionID, "busy");
|
|
4767
|
+
}
|
|
4768
|
+
await hooks.event?.({ event: normalizeV2Event(event) });
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4771
|
+
function normalizeV2Event(event) {
|
|
4772
|
+
const properties = event?.data && typeof event.data === "object" ? event.data : {};
|
|
4773
|
+
if (event?.type === "session.compaction.ended")
|
|
4774
|
+
return { type: "session.compacted", properties };
|
|
4775
|
+
if (event?.type === "session.execution.failed")
|
|
4776
|
+
return { type: "session.error", properties };
|
|
4777
|
+
if (event?.type === "session.message.content.updated") {
|
|
4778
|
+
return {
|
|
4779
|
+
type: "message.part.updated",
|
|
4780
|
+
properties: {
|
|
4781
|
+
sessionID: properties.sessionID,
|
|
4782
|
+
part: { messageID: properties.messageID }
|
|
4783
|
+
}
|
|
4784
|
+
};
|
|
4785
|
+
}
|
|
4786
|
+
return { type: event?.type, properties };
|
|
4787
|
+
}
|
|
4483
4788
|
var plugin_default = {
|
|
4484
4789
|
id: PLUGIN_ID,
|
|
4485
4790
|
server,
|