@prevalentware/opencode-goal-plugin 0.1.35 → 0.1.36
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 +118 -12
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -152,12 +152,20 @@ var PendingAttemptSchema = Schema.Struct({
|
|
|
152
152
|
armNoProgress: Schema.Boolean,
|
|
153
153
|
previousLastContinuationAt: Schema.NullOr(Schema.Number)
|
|
154
154
|
});
|
|
155
|
+
var UsageTrackerSchema = Schema.Struct({
|
|
156
|
+
baseline: Schema.optionalWith(Schema.Unknown, { default: () => null }),
|
|
157
|
+
lastObserved: Schema.optionalWith(Schema.Unknown, { default: () => null }),
|
|
158
|
+
baseTokens: Schema.optionalWith(Schema.Unknown, { default: () => null }),
|
|
159
|
+
pendingBaseline: Schema.optionalWith(Schema.Unknown, { default: () => null }),
|
|
160
|
+
pendingBaseTokens: Schema.optionalWith(Schema.Unknown, { default: () => null })
|
|
161
|
+
});
|
|
155
162
|
var GoalSchema = Schema.Struct({
|
|
156
163
|
sessionID: Schema.String,
|
|
157
164
|
objective: Schema.String,
|
|
158
165
|
status: Schema.Literal("active", "paused", "budgetLimited", "usageLimited", "complete", "unmet"),
|
|
159
166
|
tokenBudget: NullableNumber,
|
|
160
167
|
tokensUsed: Schema.Number,
|
|
168
|
+
usageTrackers: Schema.optionalWith(Schema.Record({ key: Schema.String, value: UsageTrackerSchema }), { default: () => ({}) }),
|
|
161
169
|
timeUsedSeconds: Schema.Number,
|
|
162
170
|
createdAt: Schema.Number,
|
|
163
171
|
updatedAt: Schema.Number,
|
|
@@ -315,12 +323,34 @@ function normalizeGoal(goal) {
|
|
|
315
323
|
goal.maxAutoTurns = positiveIntegerOrNull(goal.maxAutoTurns);
|
|
316
324
|
goal.maxDurationSeconds = positiveIntegerOrNull(goal.maxDurationSeconds);
|
|
317
325
|
goal.tokenBudget = positiveIntegerOrNull(goal.tokenBudget);
|
|
326
|
+
goal.usageTrackers = normalizeUsageTrackers(goal.usageTrackers);
|
|
318
327
|
goal.noProgressTokenThreshold = positiveIntegerOrNull(goal.noProgressTokenThreshold) ?? DEFAULT_NO_PROGRESS_TOKEN_THRESHOLD;
|
|
319
328
|
goal.maxNoProgressTurns = positiveIntegerOrNull(goal.maxNoProgressTurns) ?? DEFAULT_MAX_NO_PROGRESS_TURNS;
|
|
320
329
|
goal.budgetWrapupSent = goal.budgetWrapupSent === true;
|
|
321
330
|
goal.stopReason ??= null;
|
|
322
331
|
return goal;
|
|
323
332
|
}
|
|
333
|
+
function normalizeUsageTrackers(trackers) {
|
|
334
|
+
const normalized = {};
|
|
335
|
+
for (const [source, rawTracker] of Object.entries(trackers ?? {})) {
|
|
336
|
+
const tracker = rawTracker;
|
|
337
|
+
const baseline = nonNegativeIntegerOrNull(tracker?.baseline);
|
|
338
|
+
const lastObserved = nonNegativeIntegerOrNull(tracker?.lastObserved);
|
|
339
|
+
const baseTokens = nonNegativeIntegerOrNull(tracker?.baseTokens);
|
|
340
|
+
if (source && baseline != null && lastObserved != null && baseTokens != null && lastObserved >= baseline) {
|
|
341
|
+
const pendingBaseline = nonNegativeIntegerOrNull(tracker.pendingBaseline);
|
|
342
|
+
const pendingBaseTokens = nonNegativeIntegerOrNull(tracker.pendingBaseTokens);
|
|
343
|
+
normalized[source] = {
|
|
344
|
+
baseline,
|
|
345
|
+
lastObserved,
|
|
346
|
+
baseTokens,
|
|
347
|
+
pendingBaseline,
|
|
348
|
+
pendingBaseTokens: pendingBaseline == null ? null : pendingBaseTokens
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return normalized;
|
|
353
|
+
}
|
|
324
354
|
function normalizePendingAttempt(attempt) {
|
|
325
355
|
if (!attempt || typeof attempt !== "object")
|
|
326
356
|
return null;
|
|
@@ -365,6 +395,9 @@ function positiveIntegerOrNull(value) {
|
|
|
365
395
|
function nonNegativeInteger(value, fallback) {
|
|
366
396
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : fallback;
|
|
367
397
|
}
|
|
398
|
+
function nonNegativeIntegerOrNull(value) {
|
|
399
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
400
|
+
}
|
|
368
401
|
function isClosed(status) {
|
|
369
402
|
return status === "complete" || status === "unmet";
|
|
370
403
|
}
|
|
@@ -444,6 +477,7 @@ async function createGoal(sessionID, objective, options) {
|
|
|
444
477
|
status: normalizedOptions.initialStatus,
|
|
445
478
|
tokenBudget: normalizedOptions.tokenBudget,
|
|
446
479
|
tokensUsed: 0,
|
|
480
|
+
usageTrackers: {},
|
|
447
481
|
timeUsedSeconds: 0,
|
|
448
482
|
createdAt: now,
|
|
449
483
|
updatedAt: now,
|
|
@@ -605,14 +639,66 @@ async function clearGoal(sessionID) {
|
|
|
605
639
|
return existed;
|
|
606
640
|
});
|
|
607
641
|
}
|
|
608
|
-
async function accountUsage(sessionID, tokensUsed) {
|
|
642
|
+
async function accountUsage(sessionID, tokensUsed, options) {
|
|
609
643
|
return mutate((state) => {
|
|
610
644
|
const goal = state.goals[sessionID];
|
|
611
645
|
if (!goal)
|
|
612
646
|
return null;
|
|
613
647
|
accountWallClock(goal);
|
|
614
648
|
if (typeof tokensUsed === "number" && Number.isFinite(tokensUsed)) {
|
|
615
|
-
|
|
649
|
+
const observed = Math.max(0, Math.ceil(tokensUsed));
|
|
650
|
+
if (options?.cumulative === true) {
|
|
651
|
+
const source = options.source?.trim() || "default";
|
|
652
|
+
let tracker = goal.usageTrackers[source];
|
|
653
|
+
if (!tracker) {
|
|
654
|
+
const initialBaseline = nonNegativeIntegerOrNull(options.initialBaseline);
|
|
655
|
+
tracker = initialBaseline != null && initialBaseline <= observed ? {
|
|
656
|
+
baseline: initialBaseline,
|
|
657
|
+
lastObserved: observed,
|
|
658
|
+
baseTokens: goal.tokensUsed,
|
|
659
|
+
pendingBaseline: null,
|
|
660
|
+
pendingBaseTokens: null
|
|
661
|
+
} : {
|
|
662
|
+
baseline: observed,
|
|
663
|
+
lastObserved: observed,
|
|
664
|
+
baseTokens: goal.tokensUsed,
|
|
665
|
+
pendingBaseline: null,
|
|
666
|
+
pendingBaseTokens: null
|
|
667
|
+
};
|
|
668
|
+
goal.usageTrackers[source] = tracker;
|
|
669
|
+
} else if (observed < tracker.lastObserved) {
|
|
670
|
+
const initialBaseline = nonNegativeIntegerOrNull(options.initialBaseline);
|
|
671
|
+
if (initialBaseline != null && initialBaseline <= observed) {
|
|
672
|
+
tracker = {
|
|
673
|
+
baseline: initialBaseline,
|
|
674
|
+
lastObserved: observed,
|
|
675
|
+
baseTokens: goal.tokensUsed,
|
|
676
|
+
pendingBaseline: null,
|
|
677
|
+
pendingBaseTokens: null
|
|
678
|
+
};
|
|
679
|
+
goal.usageTrackers[source] = tracker;
|
|
680
|
+
} else if (tracker.pendingBaseline == null || observed < tracker.pendingBaseline) {
|
|
681
|
+
tracker.pendingBaseline = observed;
|
|
682
|
+
tracker.pendingBaseTokens = goal.tokensUsed;
|
|
683
|
+
} else {
|
|
684
|
+
tracker = {
|
|
685
|
+
baseline: tracker.pendingBaseline,
|
|
686
|
+
lastObserved: observed,
|
|
687
|
+
baseTokens: tracker.pendingBaseTokens ?? goal.tokensUsed,
|
|
688
|
+
pendingBaseline: null,
|
|
689
|
+
pendingBaseTokens: null
|
|
690
|
+
};
|
|
691
|
+
goal.usageTrackers[source] = tracker;
|
|
692
|
+
}
|
|
693
|
+
} else {
|
|
694
|
+
tracker.lastObserved = observed;
|
|
695
|
+
tracker.pendingBaseline = null;
|
|
696
|
+
tracker.pendingBaseTokens = null;
|
|
697
|
+
}
|
|
698
|
+
goal.tokensUsed = Math.max(goal.tokensUsed, tracker.baseTokens + observed - tracker.baseline);
|
|
699
|
+
} else {
|
|
700
|
+
goal.tokensUsed = Math.max(goal.tokensUsed, observed);
|
|
701
|
+
}
|
|
616
702
|
}
|
|
617
703
|
maybeStopForBudget(goal);
|
|
618
704
|
goal.updatedAt = nowSeconds();
|
|
@@ -1097,7 +1183,7 @@ function commandNameFromOptions(options) {
|
|
|
1097
1183
|
function positiveIntegerOrNull2(value) {
|
|
1098
1184
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
1099
1185
|
}
|
|
1100
|
-
function
|
|
1186
|
+
function nonNegativeIntegerOrNull2(value) {
|
|
1101
1187
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
1102
1188
|
}
|
|
1103
1189
|
function timeoutMillisecondsFromSeconds(value) {
|
|
@@ -1190,9 +1276,9 @@ function outputTokensFromMessage(message) {
|
|
|
1190
1276
|
return outputTokensFromRecord(message.info.tokens);
|
|
1191
1277
|
return;
|
|
1192
1278
|
}
|
|
1193
|
-
function
|
|
1279
|
+
function usageFromMessages(messages) {
|
|
1194
1280
|
const exactTotal = messages.reduce((sum, message) => sum + (exactTokensFromMessage(message) ?? 0), 0);
|
|
1195
|
-
return exactTotal > 0 ? exactTotal : estimateMessages(messages);
|
|
1281
|
+
return exactTotal > 0 ? { tokens: exactTotal, source: "v1.messages.exact" } : { tokens: estimateMessages(messages), source: "v1.messages.estimated" };
|
|
1196
1282
|
}
|
|
1197
1283
|
function taskHeader(output) {
|
|
1198
1284
|
const resultIndex = output.search(/<task_(?:result|error)>/);
|
|
@@ -1696,6 +1782,7 @@ async function createGoalFromTool(input, context, services) {
|
|
|
1696
1782
|
agent: typeof context.agent === "string" ? context.agent : null,
|
|
1697
1783
|
initialStatus: planningOnly ? "paused" : "active"
|
|
1698
1784
|
});
|
|
1785
|
+
await services.initializeUsage?.(context.sessionID);
|
|
1699
1786
|
return JSON.stringify(planningOnly ? { goal, plan_mode_notice: PLAN_MODE_CREATE_NOTICE } : { goal }, null, 2);
|
|
1700
1787
|
}
|
|
1701
1788
|
async function updateGoalObjectiveFromTool(input, context, services) {
|
|
@@ -1758,7 +1845,7 @@ var server = async ({ client }, options) => {
|
|
|
1758
1845
|
const autoContinue = options?.auto_continue ?? true;
|
|
1759
1846
|
const deferWhileTasksActive = options?.defer_while_tasks_active ?? true;
|
|
1760
1847
|
const maxAutoTurns = positiveIntegerOrNull2(options?.max_auto_turns) ?? DEFAULT_MAX_AUTO_TURNS;
|
|
1761
|
-
const minInterval =
|
|
1848
|
+
const minInterval = nonNegativeIntegerOrNull2(options?.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
|
|
1762
1849
|
const maxTurnTimeMs = timeoutMillisecondsFromSeconds(options?.max_turn_time);
|
|
1763
1850
|
const maxPromptFailures = positiveIntegerOrNull2(options?.max_prompt_failures) ?? DEFAULT_MAX_PROMPT_FAILURES;
|
|
1764
1851
|
const registerCommand = options?.register_command ?? true;
|
|
@@ -2167,7 +2254,8 @@ var server = async ({ client }, options) => {
|
|
|
2167
2254
|
const sessionID = "sessionID" in input && typeof input.sessionID === "string" ? input.sessionID : output.messages.find((message) => typeof message.info.sessionID === "string")?.info.sessionID;
|
|
2168
2255
|
if (!sessionID)
|
|
2169
2256
|
return;
|
|
2170
|
-
|
|
2257
|
+
const usage = usageFromMessages(output.messages);
|
|
2258
|
+
await accountUsage(sessionID, usage.tokens, { cumulative: true, source: usage.source });
|
|
2171
2259
|
const observed = await recordAssistantMessage(sessionID, latestAssistantMessage(output.messages), options ?? {});
|
|
2172
2260
|
await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, observed.goal);
|
|
2173
2261
|
const scheduled = scheduledContinuations.get(sessionID);
|
|
@@ -2298,7 +2386,7 @@ async function setupV2(context) {
|
|
|
2298
2386
|
const autoContinue = options.auto_continue ?? true;
|
|
2299
2387
|
const deferWhileTasksActive = options.defer_while_tasks_active ?? true;
|
|
2300
2388
|
const maxAutoTurns = positiveIntegerOrNull2(options.max_auto_turns) ?? DEFAULT_MAX_AUTO_TURNS;
|
|
2301
|
-
const minInterval =
|
|
2389
|
+
const minInterval = nonNegativeIntegerOrNull2(options.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
|
|
2302
2390
|
const maxTurnTimeMs = timeoutMillisecondsFromSeconds(options.max_turn_time);
|
|
2303
2391
|
const maxPromptFailures = positiveIntegerOrNull2(options.max_prompt_failures) ?? DEFAULT_MAX_PROMPT_FAILURES;
|
|
2304
2392
|
const registerCommand = options.register_command ?? true;
|
|
@@ -2314,11 +2402,21 @@ async function setupV2(context) {
|
|
|
2314
2402
|
const toolAttempts = new Map;
|
|
2315
2403
|
const planAgents = restrictedAgentSet(options);
|
|
2316
2404
|
const isPlanAgent = (agent) => typeof agent === "string" && planAgents.has(agent.trim().toLowerCase());
|
|
2317
|
-
const goalServices = { options, isPlanAgent };
|
|
2318
2405
|
const activeContinuationsV2 = new Set;
|
|
2319
2406
|
const latestStepBySession = new Map;
|
|
2320
2407
|
const stepTextBuffers = new Map;
|
|
2321
2408
|
const stepTokenSums = new Map;
|
|
2409
|
+
const goalServices = {
|
|
2410
|
+
options,
|
|
2411
|
+
isPlanAgent,
|
|
2412
|
+
initializeUsage: async (sessionID) => {
|
|
2413
|
+
try {
|
|
2414
|
+
await accountUsage(sessionID, stepTokenSums.get(sessionID) ?? 0, { cumulative: true, source: "v2.steps" });
|
|
2415
|
+
} catch (error) {
|
|
2416
|
+
v2ErrorLog("Failed to initialize goal usage accounting", error);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
};
|
|
2322
2420
|
const registrations = [];
|
|
2323
2421
|
let disposed = false;
|
|
2324
2422
|
function stepKey(sessionID, messageID2) {
|
|
@@ -2719,7 +2817,11 @@ async function setupV2(context) {
|
|
|
2719
2817
|
if (typeof tokens === "number") {
|
|
2720
2818
|
const sum = (stepTokenSums.get(sessionID) ?? 0) + tokens;
|
|
2721
2819
|
stepTokenSums.set(sessionID, sum);
|
|
2722
|
-
await accountUsage(sessionID, sum
|
|
2820
|
+
await accountUsage(sessionID, sum, {
|
|
2821
|
+
cumulative: true,
|
|
2822
|
+
source: "v2.steps",
|
|
2823
|
+
initialBaseline: Math.ceil(sum - tokens)
|
|
2824
|
+
});
|
|
2723
2825
|
}
|
|
2724
2826
|
const text = stepTextBuffers.get(stepKey(sessionID, messageID2)) ?? "";
|
|
2725
2827
|
stepTextBuffers.delete(stepKey(sessionID, messageID2));
|
|
@@ -2755,7 +2857,11 @@ async function setupV2(context) {
|
|
|
2755
2857
|
if (typeof tokens === "number") {
|
|
2756
2858
|
const sum = (stepTokenSums.get(sessionID) ?? 0) + tokens;
|
|
2757
2859
|
stepTokenSums.set(sessionID, sum);
|
|
2758
|
-
await accountUsage(sessionID, sum
|
|
2860
|
+
await accountUsage(sessionID, sum, {
|
|
2861
|
+
cumulative: true,
|
|
2862
|
+
source: "v2.steps",
|
|
2863
|
+
initialBaseline: Math.ceil(sum - tokens)
|
|
2864
|
+
});
|
|
2759
2865
|
}
|
|
2760
2866
|
const text = stepTextBuffers.get(stepKey(sessionID, messageID2)) ?? "";
|
|
2761
2867
|
stepTextBuffers.delete(stepKey(sessionID, messageID2));
|
|
@@ -2788,7 +2894,7 @@ async function setupV2(context) {
|
|
|
2788
2894
|
return;
|
|
2789
2895
|
const tokens = tokensFromRecord(data.tokens);
|
|
2790
2896
|
if (typeof tokens === "number")
|
|
2791
|
-
await accountUsage(sessionID, tokens);
|
|
2897
|
+
await accountUsage(sessionID, tokens, { cumulative: true, source: "v2.session" });
|
|
2792
2898
|
return;
|
|
2793
2899
|
}
|
|
2794
2900
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prevalentware/opencode-goal-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
4
4
|
"description": "OpenCode goal plugin that adds Codex-style long-running goal mode, /goal commands, persistence, and TUI status for AI coding agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode",
|