@ljwei-stak/dsh-model-router 0.23.0 → 0.24.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/.dsh-plugin/client.js +118 -45
- package/.dsh-plugin/index.mjs +318 -59
- package/.dsh-plugin/shared/budget-reservations.mjs +254 -0
- package/.dsh-plugin/shared/execution-lifecycle.mjs +141 -0
- package/.dsh-plugin/shared/official-team-runtime.mjs +37 -13
- package/.dsh-plugin/shared/official-tools-remote.mjs +5 -4
- package/.dsh-plugin/shared/official-tools-runtime.mjs +3 -2
- package/.dsh-plugin/shared/router-state.mjs +23 -8
- package/.dsh-plugin/shared/run-ledger.mjs +6 -1
- package/.dsh-plugin/shared/task-executors.mjs +32 -3
- package/.dsh-plugin/shared/zcode-bundle.mjs +3 -3
- package/CHANGELOG.md +20 -0
- package/README.i18n.yaml +2 -2
- package/README.md +27 -17
- package/README.zh.md +26 -16
- package/docs/WORKBENCH_USER_GUIDE.zh.md +3 -1
- package/docs/assets/workbench-budget-reservations.png +0 -0
- package/docs/assets/workbench-run-timeout.png +0 -0
- package/docs/releases/0.23.1-validation.md +23 -0
- package/docs/releases/0.24.0-validation.md +41 -0
- package/package.json +9 -3
package/.dsh-plugin/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from 'node:os'
|
|
|
4
4
|
import { isAbsolute } from 'node:path'
|
|
5
5
|
import z from '@deepseek-ai/schemastery'
|
|
6
6
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
7
|
-
import { DEFAULT_ROUTER_SETTINGS, modelMetadata } from './shared/router.mjs'
|
|
7
|
+
import { DEFAULT_ROUTER_SETTINGS, estimateAssignedPackage, modelMetadata } from './shared/router.mjs'
|
|
8
8
|
import { DEFAULT_ROUTING_PRESET, normalizeRoutingPreset, routingPreset } from './shared/routing-presets.mjs'
|
|
9
9
|
import { HEALTH_CACHE_MS, apiKeyEnvPresent, healthCache, runHealthCheck, subscriptionLoginOf } from './shared/tool-health.mjs'
|
|
10
10
|
import { latestVersions } from './shared/latest-versions.mjs'
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
} from './shared/subscription-billing.mjs'
|
|
14
14
|
import { createRouterState } from './shared/router-state.mjs'
|
|
15
15
|
import { createRunJobs } from './shared/run-jobs.mjs'
|
|
16
|
+
import { createBudgetReservations, reservationSpending } from './shared/budget-reservations.mjs'
|
|
17
|
+
import { beginExecutionLifecycle } from './shared/execution-lifecycle.mjs'
|
|
16
18
|
import { applyFeedbackProfile, buildFeedbackProfile } from './shared/adaptive-feedback.mjs'
|
|
17
19
|
import { buildRoutingEvidence, parseRoutingEvidenceJson } from './shared/routing-evidence.mjs'
|
|
18
20
|
import { apiReasoningRequest } from './shared/reasoning-execution.mjs'
|
|
@@ -21,7 +23,7 @@ import { applyPricingSnapshot, createDynamicDataRefresher, dynamicDataSummary, r
|
|
|
21
23
|
import { projectDynamicData } from './shared/dynamic-data-view.mjs'
|
|
22
24
|
import {
|
|
23
25
|
budgetCheck, buildRunRecord, formatUsd, buildTeamRunRecord, buildToolRunRecord, billingOf, mergeRerun,
|
|
24
|
-
spending, storedResults, actualCost, hasStartedQualityAttempts, teamExecutionResults,
|
|
26
|
+
spending, storedResults, actualCost, hasStartedQualityAttempts, teamExecutionResults, plannedPackages,
|
|
25
27
|
} from './shared/run-ledger.mjs'
|
|
26
28
|
import { routeBoundaries } from './shared/security-boundaries.mjs'
|
|
27
29
|
import { createPlanFromRoutes } from './shared/harness-plan.mjs'
|
|
@@ -377,13 +379,81 @@ function budgetSettings(config) {
|
|
|
377
379
|
}
|
|
378
380
|
}
|
|
379
381
|
|
|
380
|
-
function budgetFor(config, saved, estimateUsd) {
|
|
382
|
+
function budgetFor(config, saved, estimateUsd, { checkEstimate = true } = {}) {
|
|
381
383
|
const settings = budgetSettings(config)
|
|
382
384
|
if ((saved.storageAvailable === false || saved.historyVerified === false) && (settings.dailyLimitUsd > 0 || settings.monthlyLimitUsd > 0)) {
|
|
383
385
|
throw new Error('无法读取费用历史,不能核验日/月预算;请恢复存储连接后再执行。')
|
|
384
386
|
}
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
+
const recorded = spending(saved.runs, undefined, saved.archivedSpending)
|
|
388
|
+
const reserved = reservationSpending(saved)
|
|
389
|
+
const spent = { ...recorded, today: recorded.today + reserved.today, month: recorded.month + reserved.month,
|
|
390
|
+
unknownToday: recorded.unknownToday + reserved.unknownToday, unknownMonth: recorded.unknownMonth + reserved.unknownMonth }
|
|
391
|
+
const check = budgetCheck({ estimateUsd, spent, ...settings })
|
|
392
|
+
if (check.limited && (checkEstimate && !check.estimateKnown || settings.dailyLimitUsd > 0 && spent.unknownToday > 0
|
|
393
|
+
|| settings.monthlyLimitUsd > 0 && spent.unknownMonth > 0) && !check.exceeded) {
|
|
394
|
+
check.exceeded = 'unknown'
|
|
395
|
+
check.message = '存在未核对费用或缺少单价,无法核验预算;请先核对账单或明确确认继续。'
|
|
396
|
+
}
|
|
397
|
+
return { ...check, spent, reserved, ...settings }
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function pausedExecutionBudget(lifecycle) {
|
|
401
|
+
if (['history-incomplete', 'active-run', 'reservation-capacity'].includes(lifecycle.reason)) {
|
|
402
|
+
throw new Error(lifecycle.reason === 'active-run' ? '该运行仍在执行或费用待核对,不能重复启动。'
|
|
403
|
+
: lifecycle.reason === 'history-incomplete' ? '费用历史不完整,不能核验执行预算。' : '待核对运行过多,请先核对执行记录和费用。')
|
|
404
|
+
}
|
|
405
|
+
return { ...lifecycle.budget, paused: true, exceeded: lifecycle.budget?.exceeded ?? 'unknown' }
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function packageEstimates(plan, task) {
|
|
409
|
+
return plannedPackages(plan, task).map(item => ({ id: item.id, estimatedCostUsd: item.estimatedCost ?? null }))
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Reprice a subscription fallback against the route that will actually bill. */
|
|
413
|
+
async function reserveChangedApiRoute(lifecycle, expected, route, task, config, confirmOverBudget) {
|
|
414
|
+
if (expected?.provider === route.provider && expected?.model === route.model) return null
|
|
415
|
+
const id = lifecycle.activePackageId()
|
|
416
|
+
if (!id) throw new Error('缺少执行检查点,不能启动 API 路线。')
|
|
417
|
+
const saved = await routingSavedState(config)
|
|
418
|
+
const estimate = createPlanFromRoutes(task, [route], { ...planDataOptions(saved, config),
|
|
419
|
+
mode: 'direct', directProvider: route.provider, directModel: route.model }).estimatedCost
|
|
420
|
+
const reserved = await lifecycle.reserveExtra(id, estimate, { confirmOverBudget: confirmOverBudget === true })
|
|
421
|
+
if (reserved.accepted) return null
|
|
422
|
+
return { ok: false, notStarted: true, error: `实际 API 路线预算未确认,已停止调用。${reserved.budget.message}` }
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** A changed budget stops before dispatch; storage failures retain their hold. */
|
|
426
|
+
async function stopExecutionLifecycle(lifecycle, error, run = lifecycle.current('interrupted')) {
|
|
427
|
+
if (error?.code === 'ROUTER_BUDGET_CHANGED') {
|
|
428
|
+
run.status = 'paused-budget'
|
|
429
|
+
run.error = error.message
|
|
430
|
+
run.finishedAt = Date.now()
|
|
431
|
+
try { await lifecycle.finish(run); error.runId = run.id; return true }
|
|
432
|
+
catch { await lifecycle.interrupt(run); return false }
|
|
433
|
+
}
|
|
434
|
+
await lifecycle.interrupt(run)
|
|
435
|
+
return false
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** The independent-opinion tool uses the same durable API budget boundary. */
|
|
439
|
+
export async function executeBudgetedConsult(ctx, route, task, config = {}, { signal, limit = 12_000, confirmOverBudget = false } = {}) {
|
|
440
|
+
const problem = taskTextProblem(text(task))
|
|
441
|
+
if (problem) throw new Error(problem)
|
|
442
|
+
const plan = createPlanFromRoutes(task, [route], { mode: 'direct', directProvider: route.provider, directModel: route.model })
|
|
443
|
+
const id = randomUUID(), at = Date.now()
|
|
444
|
+
const record = (status, results) => buildRunRecord({ id, createdAt: at, finishedAt: Date.now(), task, plan,
|
|
445
|
+
execution: { status, packages: results }, workspace: process.cwd(), pricingFor: () => route.pricing ?? null, billingFor: () => 'api' })
|
|
446
|
+
const lifecycle = await beginExecutionLifecycle({ state: routerStateStore(), buildRun: ({ status, results }) => record(status, results),
|
|
447
|
+
packageEstimates: packageEstimates(plan, task), settings: budgetSettings(config), confirmOverBudget })
|
|
448
|
+
if (!lifecycle.accepted) return { status: 'paused-budget', paused: true, budget: pausedExecutionBudget(lifecycle), ok: false }
|
|
449
|
+
let result
|
|
450
|
+
try {
|
|
451
|
+
await lifecycle.onCheckpointStart({ id: 'direct', provider: route.provider, model: route.model, channel: 'harness-llm' })
|
|
452
|
+
result = await consultConfiguredModel(ctx, route, task, limit, signal)
|
|
453
|
+
await lifecycle.onCheckpointResult({ ...result, id: 'direct', provider: route.provider, model: route.model, channel: 'harness-llm' })
|
|
454
|
+
await lifecycle.finish(record(result.cancelled ? 'cancelled' : result.ok ? 'completed' : 'failed', lifecycle.results()))
|
|
455
|
+
} catch (error) { await stopExecutionLifecycle(lifecycle, error); throw error }
|
|
456
|
+
return { ...result, runId: id }
|
|
387
457
|
}
|
|
388
458
|
|
|
389
459
|
/**
|
|
@@ -674,7 +744,7 @@ function reviewScore(answer) {
|
|
|
674
744
|
* 质量回路: a stronger configured model spot-checks answers from cheaper
|
|
675
745
|
* routes. `sample` reviews a random share; `always` reviews every cheaper answer.
|
|
676
746
|
*/
|
|
677
|
-
export async function reviewRunPackages(ctx, run, routes, config, { signal, random = Math.random } = {}) {
|
|
747
|
+
export async function reviewRunPackages(ctx, run, routes, config, { signal, random = Math.random, lifecycle } = {}) {
|
|
678
748
|
const mode = valueOf(config, 'reviewMode', 'off')
|
|
679
749
|
if (mode !== 'sample' && mode !== 'always') return []
|
|
680
750
|
const rate = Math.min(1, Math.max(0, finiteNumber(valueOf(config, 'reviewSampleRate', 0.2), 0.2)))
|
|
@@ -696,6 +766,24 @@ export async function reviewRunPackages(ctx, run, routes, config, { signal, rand
|
|
|
696
766
|
`工作包:${item.name}\n${item.objective.slice(0, 1_500)}`,
|
|
697
767
|
`待审结果:\n${item.answer.slice(0, 3_500)}`,
|
|
698
768
|
].join('\n\n')
|
|
769
|
+
const reviewId = `review:${item.id}`
|
|
770
|
+
let pendingReview = null
|
|
771
|
+
if (lifecycle) {
|
|
772
|
+
const estimate = createPlanFromRoutes(prompt, [reviewer], { mode: 'direct', directProvider: reviewer.provider,
|
|
773
|
+
directModel: reviewer.model }).estimatedCost
|
|
774
|
+
const reserved = await lifecycle.reserveExtra(reviewId, estimate)
|
|
775
|
+
if (!reserved.accepted) { item.review = { score: null, summary: '预算未确认,已跳过额外审阅。', at: Date.now() }; continue }
|
|
776
|
+
pendingReview = { id: reviewId, packageId: item.id, provider: reviewer.provider, model: reviewer.model,
|
|
777
|
+
status: 'running', inFlight: true, ran: false, costUsd: null, billing: 'api', finishedAt: Date.now() }
|
|
778
|
+
run.reviews.push(pendingReview)
|
|
779
|
+
try { await lifecycle.saveRun(run, { startedIds: [reviewId] }) }
|
|
780
|
+
catch (error) {
|
|
781
|
+
if (error.code !== 'ROUTER_BUDGET_CHANGED') throw error
|
|
782
|
+
run.reviews = run.reviews.filter(entry => entry !== pendingReview)
|
|
783
|
+
item.review = { score: null, summary: '预算已变化,已跳过额外审阅。', at: Date.now() }
|
|
784
|
+
break
|
|
785
|
+
}
|
|
786
|
+
}
|
|
699
787
|
try {
|
|
700
788
|
const result = await consultConfiguredModel(ctx, reviewer, prompt, 1_200, signal)
|
|
701
789
|
const score = result.ok ? reviewScore(result.answer) : null
|
|
@@ -707,9 +795,16 @@ export async function reviewRunPackages(ctx, run, routes, config, { signal, rand
|
|
|
707
795
|
}
|
|
708
796
|
const cost = actualCost(result, reviewer.pricing ?? null)
|
|
709
797
|
const entry = { packageId: item.id, provider: reviewer.provider, model: reviewer.model, ran: true, finishedAt: Date.now(), usage: result.usage ?? null, ...cost, billing: 'api', referenceCostUsd: null }
|
|
710
|
-
|
|
798
|
+
if (pendingReview) Object.assign(pendingReview, entry, { status: 'completed', inFlight: false })
|
|
799
|
+
else run.reviews.push(entry)
|
|
711
800
|
reviews.push({ ...entry, score })
|
|
801
|
+
if (lifecycle) await lifecycle.saveRun(run, { completedIds: [reviewId] })
|
|
712
802
|
} catch (error) {
|
|
803
|
+
if (error.code === 'ROUTER_CHECKPOINT_FAILED') throw error
|
|
804
|
+
if (pendingReview) {
|
|
805
|
+
Object.assign(pendingReview, { status: 'failed', inFlight: false, ran: true, costUsd: null, finishedAt: Date.now() })
|
|
806
|
+
await lifecycle.saveRun(run, { completedIds: [reviewId] })
|
|
807
|
+
}
|
|
713
808
|
if (signal?.aborted) break
|
|
714
809
|
item.review = { provider: reviewer.provider, model: reviewer.model, score: null, summary: `审阅失败:${errorText(error).slice(0, 200)}`, at: Date.now() }
|
|
715
810
|
}
|
|
@@ -724,6 +819,8 @@ async function reportRunProgress(options, event) {
|
|
|
724
819
|
|
|
725
820
|
function executionHooks(ctx, options) {
|
|
726
821
|
return {
|
|
822
|
+
onCheckpointStart: options.onCheckpointStart,
|
|
823
|
+
onCheckpointResult: options.onCheckpointResult,
|
|
727
824
|
skipOfficial: options.skipOfficial ?? (request => healthCache.skipReason(request)),
|
|
728
825
|
onStage: stage => reportRunProgress(options, { type: 'stage', stage }),
|
|
729
826
|
onPackageStart: async item => {
|
|
@@ -877,9 +974,24 @@ export async function executeConfiguredAssignment(ctx, task, config = {}, option
|
|
|
877
974
|
const billing = await billingContext(config, options)
|
|
878
975
|
const limit = boundedInteger(valueOf(config, 'maxConsultOutputChars', 12_000), 12_000, 500, 50_000)
|
|
879
976
|
const startedAt = Date.now()
|
|
977
|
+
const runId = randomUUID()
|
|
978
|
+
const makeRecord = execution => buildRunRecord({ id: runId, createdAt: startedAt, finishedAt: Date.now(),
|
|
979
|
+
task: taskText, plan, execution, preset: plan.preset ?? preset, workspace: options.workspace ?? '',
|
|
980
|
+
pricingFor: item => routePricing(routes, item), billingFor: billingForResult,
|
|
981
|
+
budget: { estimateUsd: budget.estimateUsd, exceeded: budget.exceeded, downgraded: budget.downgraded === true,
|
|
982
|
+
confirmed: options.confirmOverBudget === true } })
|
|
983
|
+
const lifecycle = await beginExecutionLifecycle({ state: routerStateStore(),
|
|
984
|
+
packageEstimates: packageEstimates(plan, taskText), settings: budgetSettings(config),
|
|
985
|
+
confirmOverBudget: options.confirmOverBudget === true,
|
|
986
|
+
buildRun: ({ status, results }) => makeRecord({ status, packages: results }) })
|
|
987
|
+
if (!lifecycle.accepted) return { plan, execution: null, paused: true, budget: pausedExecutionBudget(lifecycle) }
|
|
880
988
|
const gate = parseQualityGateJson(valueOf(config, 'qualityGateJson', ''))
|
|
881
989
|
const gatedApi = async ({ route, task: packageTask, signal, subscription }) => {
|
|
882
990
|
if (subscription) return consultConfiguredModel(ctx, route, packageTask, limit, signal)
|
|
991
|
+
const plannedPackage = plannedPackages(plan, taskText).find(item => item.id === lifecycle.activePackageId())
|
|
992
|
+
const denied = await reserveChangedApiRoute(lifecycle, { provider: plannedPackage?.recommendedProvider,
|
|
993
|
+
model: plannedPackage?.recommendedModel }, route, packageTask, config, options.confirmOverBudget)
|
|
994
|
+
if (denied) return denied
|
|
883
995
|
const quality = routeQuality(route)
|
|
884
996
|
const alternatives = gate && !direct && route.manualPinned !== true && apiUpgradeEligible(route, routes)
|
|
885
997
|
&& valueOf(config, 'qualityEscalationEnabled', false) === true && quality !== null
|
|
@@ -906,10 +1018,22 @@ export async function executeConfiguredAssignment(ctx, task, config = {}, option
|
|
|
906
1018
|
&& (result.blocked || result.notStarted || billingForResult(result) === 'subscription') ? { costUsd: 0 }
|
|
907
1019
|
: actualCost(result, routePricing(routes, result))), ...attempts.map(result => actualCost(result, result.pricingSnapshot))]
|
|
908
1020
|
const pending = plan.costBreakdown.slice(finished.length + 1).map(item => item.estimatedCost)
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1021
|
+
const unknownProjection = costs.some(item => item.costUsd === null) || pending.some(value => !Number.isFinite(value))
|
|
1022
|
+
if (unknownProjection && constrained) return false
|
|
1023
|
+
const projected = unknownProjection ? null
|
|
1024
|
+
: costs.reduce((sum, item) => sum + item.costUsd, estimate) + pending.reduce((sum, value) => sum + value, 0)
|
|
1025
|
+
if (!unknownProjection && (!Number.isFinite(projected) || perRunLimit > 0 && projected > perRunLimit)) return false
|
|
1026
|
+
const id = lifecycle.activePackageId()
|
|
1027
|
+
if (!id) return false
|
|
1028
|
+
await lifecycle.partialResult({ ...attempts.at(-1), id, ok: false, qualityAttempts: attempts })
|
|
1029
|
+
if (!(await lifecycle.reserveExtra(id, estimate)).accepted) return false
|
|
1030
|
+
// The extra request can be charged before its receipt arrives. Persist
|
|
1031
|
+
// both the first receipt and the unknown second attempt before dispatch.
|
|
1032
|
+
await lifecycle.partialResult({ ...attempts.at(-1), id, ok: false, qualityAttempts: [...attempts, {
|
|
1033
|
+
provider: to.provider, model: to.model, channel: 'harness-llm', ok: false,
|
|
1034
|
+
usage: null, usageIncomplete: true, pricingSnapshot: to.pricing ?? null, finishedAt: Date.now(),
|
|
1035
|
+
}] }, { checkBudget: true })
|
|
1036
|
+
return true
|
|
913
1037
|
},
|
|
914
1038
|
})
|
|
915
1039
|
}
|
|
@@ -922,40 +1046,49 @@ export async function executeConfiguredAssignment(ctx, task, config = {}, option
|
|
|
922
1046
|
workspace: options.workspace,
|
|
923
1047
|
signal: options.signal,
|
|
924
1048
|
...hooks,
|
|
1049
|
+
onCheckpointStart: item => lifecycle.onCheckpointStart(item),
|
|
1050
|
+
onCheckpointResult: result => lifecycle.onCheckpointResult(result),
|
|
925
1051
|
billing,
|
|
926
1052
|
apiFallback: gatedApi,
|
|
927
1053
|
})
|
|
928
1054
|
} catch (error) {
|
|
929
1055
|
// Steps that already ran (and may have been paid for) are always recorded with their spend.
|
|
930
1056
|
const message = String(error?.message ?? error)
|
|
931
|
-
const
|
|
1057
|
+
const receipts = new Map([...finished, ...lifecycle.results(), ...(error.execution?.packages ?? [])].map(item => [item.id, item]))
|
|
1058
|
+
const partial = { status: options.signal?.aborted ? 'cancelled' : 'failed', packages: [...receipts.values()], aggregate: '', error: message }
|
|
932
1059
|
const run = buildRunRecord({
|
|
933
|
-
id:
|
|
1060
|
+
id: runId, createdAt: startedAt, finishedAt: Date.now(), task: taskText, plan, execution: partial,
|
|
934
1061
|
preset: plan.preset ?? preset, workspace: options.workspace ?? '',
|
|
935
1062
|
pricingFor: item => routePricing(routes, item), billingFor: billingForResult,
|
|
936
1063
|
budget: { estimateUsd: budget.estimateUsd, exceeded: budget.exceeded, downgraded: budget.downgraded === true, confirmed: budget.exceeded ? true : undefined },
|
|
937
1064
|
})
|
|
938
1065
|
run.error = message.slice(0, 2_000)
|
|
939
1066
|
let stored = true
|
|
940
|
-
try {
|
|
1067
|
+
try {
|
|
1068
|
+
if (error.code === 'ROUTER_BUDGET_CHANGED') stored = await stopExecutionLifecycle(lifecycle, error, run)
|
|
1069
|
+
else if (error.code === 'ROUTER_CHECKPOINT_FAILED') { await lifecycle.interrupt(run); stored = false }
|
|
1070
|
+
else await lifecycle.finish(run)
|
|
1071
|
+
} catch { stored = false; await lifecycle.interrupt(run) }
|
|
941
1072
|
const wrapped = new Error(`${message}${finished.length ? `(已完成 ${finished.length} 个步骤,${stored ? `已记录到运行 ${run.id},费用计入预算` : '但运行记录保存失败'})` : ''}`)
|
|
942
1073
|
wrapped.runId = stored ? run.id : null
|
|
943
1074
|
wrapped.cause = error
|
|
944
1075
|
throw wrapped
|
|
945
1076
|
}
|
|
946
1077
|
const run = buildRunRecord({
|
|
947
|
-
id:
|
|
1078
|
+
id: runId, createdAt: startedAt, finishedAt: Date.now(), task: taskText, plan, execution,
|
|
948
1079
|
preset: plan.preset ?? preset, workspace: options.workspace ?? '',
|
|
949
1080
|
pricingFor: item => routePricing(routes, item),
|
|
950
1081
|
billingFor: billingForResult,
|
|
951
1082
|
budget: { estimateUsd: budget.estimateUsd, exceeded: budget.exceeded, downgraded: budget.downgraded === true, confirmed: budget.exceeded ? true : undefined },
|
|
952
1083
|
})
|
|
953
1084
|
await reportRunProgress(options, { type: 'stage', stage: 'reviewing' })
|
|
954
|
-
|
|
1085
|
+
let reviews
|
|
1086
|
+
try { reviews = await reviewRunPackages(ctx, run, routes, config, { signal: options.signal, random: options.random, lifecycle }) }
|
|
1087
|
+
catch (error) { await lifecycle.interrupt(run); throw error }
|
|
955
1088
|
if (options.signal?.aborted) { run.status = 'cancelled'; execution.status = 'cancelled' }
|
|
956
1089
|
await reportRunProgress(options, { type: 'stage', stage: 'saving' })
|
|
957
1090
|
let stored = true
|
|
958
|
-
try { await
|
|
1091
|
+
try { await lifecycle.finish(run) } catch { stored = false; await lifecycle.interrupt(run) }
|
|
959
1092
|
const awaiting = pausedSteps(execution)
|
|
960
1093
|
return { plan, execution, budget, runId: run.id, run, reviews, stored, ...(awaiting.length ? { awaitingConfirmation: awaiting } : {}) }
|
|
961
1094
|
}
|
|
@@ -995,8 +1128,34 @@ export function rerunSupport(run) {
|
|
|
995
1128
|
return { ok: true, kind: run.kind === 'team' ? 'team' : 'assign', writes: false }
|
|
996
1129
|
}
|
|
997
1130
|
|
|
1131
|
+
/** Estimate exactly the packages a retry will start, retaining unknown prices. */
|
|
1132
|
+
function rerunEstimates(run, ids, targetId, override, routes, dataOptions) {
|
|
1133
|
+
const estimates = new Map()
|
|
1134
|
+
for (const item of run.packages.filter(item => ids.has(item.id))) {
|
|
1135
|
+
const assigned = item.id === targetId ? override : null
|
|
1136
|
+
const provider = assigned?.provider ?? item.provider ?? item.recommendedProvider
|
|
1137
|
+
const model = assigned?.model ?? item.model ?? item.recommendedModel
|
|
1138
|
+
const changedRoute = provider !== item.recommendedProvider || model !== item.recommendedModel
|
|
1139
|
+
const reassignsRoute = assigned && (assigned.provider !== (item.provider ?? item.recommendedProvider)
|
|
1140
|
+
|| assigned.model !== (item.model ?? item.recommendedModel))
|
|
1141
|
+
let estimate = item.estimatedCost
|
|
1142
|
+
// An unchanged route can retain its planned effort; changing identity uses
|
|
1143
|
+
// the new adapter's default effort, just like rerunAssignmentPackage.
|
|
1144
|
+
if (reassignsRoute || changedRoute) {
|
|
1145
|
+
const route = routes.find(route => route.provider === provider && route.model === model)
|
|
1146
|
+
estimate = route ? estimateAssignedPackage(route, item, run.task,
|
|
1147
|
+
run.decision?.complexity?.band ?? 'balanced', dataOptions).estimatedCost : null
|
|
1148
|
+
}
|
|
1149
|
+
estimates.set(item.id, Number.isFinite(estimate) && estimate >= 0 ? estimate : null)
|
|
1150
|
+
}
|
|
1151
|
+
const values = [...estimates.values()]
|
|
1152
|
+
const total = values.every(value => value !== null) ? values.reduce((sum, value) => sum + value, 0) : null
|
|
1153
|
+
return { byId: estimates, total: Number.isFinite(total) ? total : null }
|
|
1154
|
+
}
|
|
1155
|
+
|
|
998
1156
|
export async function rerunRecordedStep(ctx, config = {}, { runId, packageId, provider, model, confirmOverBudget = false, confirmWrite = false, confirmUnsandboxed = false, subscriptionChoice = null, workspace, root, signal, ...options } = {}) {
|
|
999
1157
|
const { run, saved } = await storedRun(runId)
|
|
1158
|
+
if (saved.budgetReservations?.[run.id]) throw new Error('该运行仍在执行或费用待核对,请先核对原任务与厂商账单,不能直接重跑。')
|
|
1000
1159
|
const target = run.packages.find(item => item.id === text(packageId))
|
|
1001
1160
|
if (!target) throw new Error(`运行记录中没有工作包 ${text(packageId)}`)
|
|
1002
1161
|
const choice = text(subscriptionChoice) || null
|
|
@@ -1033,16 +1192,17 @@ export async function rerunRecordedStep(ctx, config = {}, { runId, packageId, pr
|
|
|
1033
1192
|
return { paused: true, needsConfirmation: missingConfirmations, run,
|
|
1034
1193
|
budget: null, message: '请确认续跑所需的文件修改与执行环境权限后继续;厂商 CLI 正常环境以当前用户权限运行,独立 Git 工作树不是操作系统读写边界。' }
|
|
1035
1194
|
}
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1195
|
+
const retryIds = new Set([target.id, ...downstreamPackageIds(run.packages, target.id)
|
|
1196
|
+
.filter(id => run.packages.find(item => item.id === id)?.ok !== true)])
|
|
1197
|
+
const estimates = rerunEstimates(run, retryIds, target.id, override, routes, planDataOptions(refreshed, config))
|
|
1198
|
+
const budget = budgetFor(config, saved, estimates.total)
|
|
1038
1199
|
if (budget.exceeded && confirmOverBudget !== true) {
|
|
1039
1200
|
return { paused: true, budget: { ...budget, paused: true, message: `${budget.message} 已暂停重跑,请确认后再试。` }, run }
|
|
1040
1201
|
}
|
|
1041
1202
|
const cwd = text(workspace) || run.workspace
|
|
1042
1203
|
if (!cwd || !(await fileExists(cwd))) throw new Error(`原运行的工作区 ${cwd || '(未记录)'} 不可用,无法重跑该步骤;请恢复该目录,或在原工作区的会话中调用 model_router_rerun_step。`)
|
|
1043
|
-
if (run.kind === 'team') return rerunTeamStep(ctx, { run, target, override, routes, budget,
|
|
1044
|
-
|
|
1045
|
-
.filter(id => run.packages.find(item => item.id === id)?.ok !== true)])
|
|
1204
|
+
if (run.kind === 'team') return rerunTeamStep(ctx, { run, target, override, routes, budget,
|
|
1205
|
+
ids: [...retryIds], estimates: estimates.byId, confirmOverBudget, cwd, root: text(root) || cwd, signal, options, config })
|
|
1046
1206
|
const manualRetries = Object.fromEntries(run.packages.filter(item => retryIds.has(item.id) && item.manualAssignment)
|
|
1047
1207
|
.map(item => [item.id, item.id === target.id && override ? { kind: 'api', ...override } : item.manualAssignment]))
|
|
1048
1208
|
if (Object.keys(manualRetries).length) {
|
|
@@ -1062,7 +1222,18 @@ export async function rerunRecordedStep(ctx, config = {}, { runId, packageId, pr
|
|
|
1062
1222
|
}
|
|
1063
1223
|
const limit = boundedInteger(valueOf(config, 'maxConsultOutputChars', 12_000), 12_000, 500, 50_000)
|
|
1064
1224
|
const retryGate = parseQualityGateJson(valueOf(config, 'qualityGateJson', ''))
|
|
1065
|
-
const
|
|
1225
|
+
const buildRetry = ({ status, results }) => {
|
|
1226
|
+
const next = structuredClone(run)
|
|
1227
|
+
mergeRerun(next, { status, packages: results }, { rerunIds: [...retryIds], pricingFor: item => routePricing(routes, item), billingFor: billingForResult })
|
|
1228
|
+
for (const item of next.packages) if (retryIds.has(item.id)) item.estimatedCost = estimates.byId.get(item.id) ?? null
|
|
1229
|
+
return next
|
|
1230
|
+
}
|
|
1231
|
+
const lifecycle = await beginExecutionLifecycle({ state: routerStateStore(), buildRun: buildRetry,
|
|
1232
|
+
packageEstimates: [...estimates.byId].map(([id, estimatedCostUsd]) => ({ id, estimatedCostUsd })),
|
|
1233
|
+
settings: budgetSettings(config), confirmOverBudget })
|
|
1234
|
+
if (!lifecycle.accepted) return { paused: true, run, budget: pausedExecutionBudget(lifecycle) }
|
|
1235
|
+
let execution
|
|
1236
|
+
try { execution = await rerunAssignmentPackage({
|
|
1066
1237
|
task: run.task,
|
|
1067
1238
|
packages: run.packages.map(item => ({
|
|
1068
1239
|
id: item.id, name: item.name, objective: item.objective, dependsOn: item.dependsOn,
|
|
@@ -1082,14 +1253,21 @@ export async function rerunRecordedStep(ctx, config = {}, { runId, packageId, pr
|
|
|
1082
1253
|
signal,
|
|
1083
1254
|
...(choice ? { subscriptionChoice: choice } : {}),
|
|
1084
1255
|
...executionHooks(ctx, options),
|
|
1256
|
+
onCheckpointStart: item => lifecycle.onCheckpointStart(item),
|
|
1257
|
+
onCheckpointResult: result => lifecycle.onCheckpointResult(result),
|
|
1085
1258
|
billing: await billingContext(config, options),
|
|
1086
|
-
apiFallback: async ({ route, task: packageTask, signal: callSignal, subscription }) =>
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1259
|
+
apiFallback: async ({ route, task: packageTask, signal: callSignal, subscription }) => {
|
|
1260
|
+
if (subscription) return consultConfiguredModel(ctx, route, packageTask, limit, callSignal)
|
|
1261
|
+
const id = lifecycle.activePackageId()
|
|
1262
|
+
const previous = run.packages.find(item => item.id === id)
|
|
1263
|
+
const expected = override && id === target.id ? override
|
|
1264
|
+
: { provider: previous?.provider ?? previous?.recommendedProvider, model: previous?.model ?? previous?.recommendedModel }
|
|
1265
|
+
const denied = await reserveChangedApiRoute(lifecycle, expected, route, packageTask, config, confirmOverBudget)
|
|
1266
|
+
return denied ?? consultWithQualityGate(ctx, route, packageTask, limit, callSignal, { gate: retryGate })
|
|
1267
|
+
},
|
|
1268
|
+
}) } catch (error) { await stopExecutionLifecycle(lifecycle, error); throw error }
|
|
1269
|
+
const updated = buildRetry({ status: execution.status, results: execution.packages })
|
|
1270
|
+
try { await lifecycle.finish(updated) } catch (error) { await lifecycle.interrupt(updated); throw error }
|
|
1093
1271
|
const awaiting = pausedSteps(execution)
|
|
1094
1272
|
return { run: updated, execution, budget, ...(awaiting.length ? { awaitingConfirmation: awaiting } : {}) }
|
|
1095
1273
|
}
|
|
@@ -1114,7 +1292,7 @@ async function cancelPausedStep(runId, packageId) {
|
|
|
1114
1292
|
}
|
|
1115
1293
|
|
|
1116
1294
|
/** Stored team packages in plan shape; `override` reassigns the retried package. */
|
|
1117
|
-
function teamPlanFromRun(run, ids, override, targetId) {
|
|
1295
|
+
function teamPlanFromRun(run, ids, override, targetId, estimates) {
|
|
1118
1296
|
const workPackages = run.packages.filter(item => ids.includes(item.id)).map(item => ({
|
|
1119
1297
|
id: item.id, name: item.name, objective: item.objective, dependsOn: item.dependsOn ?? [],
|
|
1120
1298
|
type: item.type ?? 'general', purpose: item.purpose ?? 'execution',
|
|
@@ -1124,7 +1302,7 @@ function teamPlanFromRun(run, ids, override, targetId) {
|
|
|
1124
1302
|
...(item.plannedReasoningEffort && !(item.id === targetId && override
|
|
1125
1303
|
&& (override.provider !== item.recommendedProvider || override.model !== item.recommendedModel))
|
|
1126
1304
|
? { recommendedReasoningEffort: item.plannedReasoningEffort } : {}),
|
|
1127
|
-
estimatedCost: item.
|
|
1305
|
+
estimatedCost: estimates.get(item.id) ?? null,
|
|
1128
1306
|
}))
|
|
1129
1307
|
return { mode: 'team', team: { workPackages } }
|
|
1130
1308
|
}
|
|
@@ -1133,13 +1311,10 @@ function teamPlanFromRun(run, ids, override, targetId) {
|
|
|
1133
1311
|
* Read-only team retry: the failed step plus downstream steps that had not
|
|
1134
1312
|
* succeeded run through the same signed runner; earlier answers are context.
|
|
1135
1313
|
*/
|
|
1136
|
-
async function rerunTeamStep(ctx, { run, target, override, routes, budget, cwd, root, signal, options, config = {} }) {
|
|
1314
|
+
async function rerunTeamStep(ctx, { run, target, override, routes, budget, ids, estimates, confirmOverBudget, cwd, root, signal, options, config = {} }) {
|
|
1137
1315
|
const mode = run.executionMode === 'workspace-write' ? 'workspace-write' : 'read-only'
|
|
1138
1316
|
const executionEnvironment = teamExecutionEnvironment(run.executionEnvironment, mode)
|
|
1139
|
-
const
|
|
1140
|
-
.filter(id => !run.packages.find(item => item.id === id)?.ok)
|
|
1141
|
-
const ids = [target.id, ...downstream]
|
|
1142
|
-
const plan = teamPlanFromRun(run, ids, override, target.id)
|
|
1317
|
+
const plan = teamPlanFromRun(run, ids, override, target.id, estimates)
|
|
1143
1318
|
const cliModels = Object.fromEntries(Object.entries(run.cliModels ?? {}).filter(([key]) => ids.includes(key) && !(override && key === target.id)))
|
|
1144
1319
|
if (override) {
|
|
1145
1320
|
const reassignedPlan = { team: { workPackages: plan.team.workPackages.filter(item => item.id === target.id) } }
|
|
@@ -1149,10 +1324,31 @@ async function rerunTeamStep(ctx, { run, target, override, routes, budget, cwd,
|
|
|
1149
1324
|
.map(item => ({ id: item.id, name: item.name, finalText: item.answer }))
|
|
1150
1325
|
const runTeam = options.runTeam ?? runOfficialTeam
|
|
1151
1326
|
const installed = Array.isArray(options.installedToolIds) ? options.installedToolIds : await installedToolIds()
|
|
1152
|
-
const
|
|
1327
|
+
const buildRetry = ({ status, results, workspace = {} }) => {
|
|
1328
|
+
const next = structuredClone(run)
|
|
1329
|
+
const converted = teamExecutionResults(plan, { status, results })
|
|
1330
|
+
if (override) {
|
|
1331
|
+
const item = converted.packages.find(item => item.id === target.id)
|
|
1332
|
+
if (item) item.reassigned = true
|
|
1333
|
+
}
|
|
1334
|
+
mergeRerun(next, converted, { rerunIds: ids, pricingFor: item => routePricing(routes, item), billingFor: billingForResult })
|
|
1335
|
+
for (const item of next.packages) if (ids.includes(item.id)) item.estimatedCost = estimates.get(item.id) ?? null
|
|
1336
|
+
next.executionEnvironment = executionEnvironment
|
|
1337
|
+
if (mode === 'workspace-write' && workspace.workspace) next.isolatedWorkspace = workspace.workspace
|
|
1338
|
+
if (workspace.baseCommit) next.baseCommit = workspace.baseCommit
|
|
1339
|
+
return next
|
|
1340
|
+
}
|
|
1341
|
+
const lifecycle = await beginExecutionLifecycle({ state: routerStateStore(), buildRun: buildRetry,
|
|
1342
|
+
packageEstimates: [...estimates].map(([id, estimatedCostUsd]) => ({ id, estimatedCostUsd })), settings: budgetSettings(config), confirmOverBudget })
|
|
1343
|
+
if (!lifecycle.accepted) return { paused: true, run, budget: pausedExecutionBudget(lifecycle) }
|
|
1344
|
+
let execution
|
|
1345
|
+
try { execution = await runTeam({ plan, task: run.task, workspace: cwd, allowedRoot: root ?? cwd, mode, executionEnvironment,
|
|
1153
1346
|
installedIds: installed, cliModels, signal, sandbox: ctx.sandbox, previous, onlyIds: ids,
|
|
1347
|
+
onCheckpointStart: item => lifecycle.onCheckpointStart(item), onCheckpointResult: result => lifecycle.onCheckpointResult(result),
|
|
1348
|
+
onWorkspace: value => lifecycle.onWorkspace(value),
|
|
1154
1349
|
...(mode === 'workspace-write' ? { seedFrom: { workspace: run.isolatedWorkspace, baseCommit: run.baseCommit } } : {}),
|
|
1155
|
-
...(options.runtime ? { runtime: options.runtime } : {}) })
|
|
1350
|
+
...(options.runtime ? { runtime: options.runtime } : {}) }) }
|
|
1351
|
+
catch (error) { await stopExecutionLifecycle(lifecycle, error); throw error }
|
|
1156
1352
|
const converted = teamExecutionResults(plan, execution)
|
|
1157
1353
|
if (override) {
|
|
1158
1354
|
const entry = converted.packages.find(item => item.id === target.id)
|
|
@@ -1164,8 +1360,12 @@ async function rerunTeamStep(ctx, { run, target, override, routes, budget, cwd,
|
|
|
1164
1360
|
const note = await noteCliQuota(config, result.toolId, `${result.error ?? ''}\n${result.outputTail ?? ''}`)
|
|
1165
1361
|
if (note) quotaNotes.push([result.id, note])
|
|
1166
1362
|
}
|
|
1167
|
-
const updated =
|
|
1168
|
-
|
|
1363
|
+
const updated = buildRetry({ status: execution.status, results: execution.results,
|
|
1364
|
+
workspace: { workspace: execution.workspace, baseCommit: execution.baseCommit } })
|
|
1365
|
+
const current = updated
|
|
1366
|
+
for (const item of current.packages) {
|
|
1367
|
+
if (ids.includes(item.id)) item.estimatedCost = estimates.get(item.id) ?? null
|
|
1368
|
+
}
|
|
1169
1369
|
for (const [id, note] of quotaNotes) {
|
|
1170
1370
|
const stored = current.packages.find(item => item.id === id)
|
|
1171
1371
|
if (stored) stored.billingSwitch = note
|
|
@@ -1185,7 +1385,7 @@ async function rerunTeamStep(ctx, { run, target, override, routes, budget, cwd,
|
|
|
1185
1385
|
if (cliModels[target.id]) bindings[target.id] = cliModels[target.id]
|
|
1186
1386
|
current.cliModels = bindings
|
|
1187
1387
|
}
|
|
1188
|
-
})
|
|
1388
|
+
try { await lifecycle.finish(updated) } catch (error) { await lifecycle.interrupt(updated); throw error }
|
|
1189
1389
|
return { run: updated, execution: { ...converted, ranIds: ids, raw: execution }, budget }
|
|
1190
1390
|
}
|
|
1191
1391
|
|
|
@@ -1211,6 +1411,7 @@ async function rerunToolRun(ctx, config, { run, confirmOverBudget, confirmWrite,
|
|
|
1211
1411
|
const cwd = text(workspace) || run.workspace
|
|
1212
1412
|
if (!cwd || !(await fileExists(cwd))) throw new Error('原运行的工作区不可用,无法重跑;请在原工作区的会话中重试。')
|
|
1213
1413
|
const { result, recorded } = await runPlannedTool(ctx, config, planned, { cwd, root: text(root) || cwd, signal, rerunOf: run.id,
|
|
1414
|
+
confirmOverBudget,
|
|
1214
1415
|
...(options.runTask ? { runTask: options.runTask } : {}) })
|
|
1215
1416
|
const execution = { status: result.status, packages: recorded?.packages ?? [], ranIds: ['direct'], raw: result }
|
|
1216
1417
|
return { run: recorded ?? run, execution, budget: planned.budget, rerunOf: run.id, newRunId: recorded?.id ?? null }
|
|
@@ -1279,16 +1480,40 @@ export async function planToolRun(ctx, config, args = {}, { signal } = {}) {
|
|
|
1279
1480
|
}
|
|
1280
1481
|
|
|
1281
1482
|
/** Run a planned tool call and record it; `rerunOf` links a rerun to the original run. */
|
|
1282
|
-
async function runPlannedTool(ctx, config, planned, { cwd, root, signal, rerunOf = null, runTask = runOfficialTask }) {
|
|
1483
|
+
async function runPlannedTool(ctx, config, planned, { cwd, root, signal, rerunOf = null, confirmOverBudget = false, runTask = runOfficialTask }) {
|
|
1283
1484
|
const startedAt = Date.now()
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1485
|
+
const runId = randomUUID()
|
|
1486
|
+
const makeToolRecord = (result, location = {}) => {
|
|
1487
|
+
const run = buildToolRunRecord({ id: runId, createdAt: startedAt, finishedAt: Date.now(),
|
|
1488
|
+
toolId: planned.toolId, toolLabel: planned.tool?.label ?? planned.toolId, task: planned.taskText,
|
|
1489
|
+
provider: planned.provider, model: planned.model, cliModel: planned.cliModel,
|
|
1490
|
+
mode: planned.mode, workspace: cwd, result, estimatedCost: planned.estimate, rerunOf,
|
|
1491
|
+
pricingFor: item => routePricing(planned.routes, item), billingFor: billingForResult, budget: planned.budget })
|
|
1492
|
+
if (location.isolatedWorkspace) run.isolatedWorkspace = location.isolatedWorkspace
|
|
1493
|
+
if (location.baseCommit) run.baseCommit = location.baseCommit
|
|
1494
|
+
return run
|
|
1495
|
+
}
|
|
1496
|
+
const lifecycle = await beginExecutionLifecycle({ state: routerStateStore(), settings: budgetSettings(config), confirmOverBudget,
|
|
1497
|
+
packageEstimates: [{ id: 'direct', estimatedCostUsd: planned.estimate ?? null }],
|
|
1498
|
+
buildRun: ({ status, results, workspace }) => makeToolRecord(results[0] ?? { status, notStarted: true }, workspace) })
|
|
1499
|
+
if (!lifecycle.accepted) {
|
|
1500
|
+
planned.budget = pausedExecutionBudget(lifecycle)
|
|
1501
|
+
return { result: { status: 'paused-budget', budget: planned.budget }, recorded: null }
|
|
1502
|
+
}
|
|
1503
|
+
let result
|
|
1504
|
+
try {
|
|
1505
|
+
result = await runTask({ toolId: planned.toolId, task: planned.taskText, modelId: planned.modelId, workspace: cwd, allowedRoot: root ?? cwd,
|
|
1506
|
+
mode: planned.mode, signal, sandbox: ctx.sandbox,
|
|
1507
|
+
onCheckpointStart: item => lifecycle.onCheckpointStart(item), onCheckpointResult: item => lifecycle.onCheckpointResult(item),
|
|
1508
|
+
onWorkspace: value => lifecycle.onWorkspace(value) })
|
|
1509
|
+
} catch (error) { await stopExecutionLifecycle(lifecycle, error); throw error }
|
|
1510
|
+
const recorded = makeToolRecord(result, { isolatedWorkspace: result.isolatedWorkspace, baseCommit: result.baseCommit })
|
|
1511
|
+
try {
|
|
1512
|
+
if (result.status !== 'succeeded') await annotateCliQuota(recorded, config,
|
|
1513
|
+
[{ id: 'direct', toolId: planned.toolId, text: `${result.error ?? result.reason ?? ''}\n${result.outputTail ?? ''}` }])
|
|
1514
|
+
await lifecycle.finish(recorded)
|
|
1515
|
+
} catch (error) { await lifecycle.interrupt(recorded); throw error }
|
|
1516
|
+
return { result, recorded: result.status === 'unsupported' ? null : recorded }
|
|
1292
1517
|
}
|
|
1293
1518
|
|
|
1294
1519
|
/** Ledger record for model_router_tool_run. */
|
|
@@ -1327,9 +1552,13 @@ export async function rateRecordedResult({ runId, packageId, rating, expectedFin
|
|
|
1327
1552
|
|
|
1328
1553
|
/** Everything the workbench shows: recent runs, spending, budget, learned biases and settings. */
|
|
1329
1554
|
export async function ledgerSummary(config = {}, { limit = 30 } = {}) {
|
|
1555
|
+
await createBudgetReservations({ state: routerStateStore() }).recoverInterruptedExecutions()
|
|
1330
1556
|
const saved = await routingSavedState(config)
|
|
1331
1557
|
const spent = spending(saved.runs, undefined, saved.archivedSpending)
|
|
1332
1558
|
const settings = budgetSettings(config)
|
|
1559
|
+
const entries = Object.values(saved.budgetReservations ?? {})
|
|
1560
|
+
const reserved = { ...reservationSpending(saved), activeCount: entries.filter(item => item.status === 'active').length,
|
|
1561
|
+
interruptedCount: entries.filter(item => item.status === 'interrupted').length }
|
|
1333
1562
|
return {
|
|
1334
1563
|
runs: saved.runs.slice(-limit).reverse().map(run => ({
|
|
1335
1564
|
...run,
|
|
@@ -1338,7 +1567,12 @@ export async function ledgerSummary(config = {}, { limit = 30 } = {}) {
|
|
|
1338
1567
|
.map(item => ({ ...item, answer: text(item.answer).slice(0, 1_500) })),
|
|
1339
1568
|
})),
|
|
1340
1569
|
spent,
|
|
1341
|
-
|
|
1570
|
+
reserved,
|
|
1571
|
+
reservations: entries.map(entry => ({ id: entry.id, runId: entry.runId, status: entry.status, updatedAt: entry.updatedAt,
|
|
1572
|
+
unknown: entry.packages.some(item => item.estimatedCostUsd === null || entry.status === 'interrupted' && item.started),
|
|
1573
|
+
remainingEstimateUsd: entry.packages.every(item => Number.isFinite(item.estimatedCostUsd))
|
|
1574
|
+
? entry.packages.reduce((sum, item) => sum + item.estimatedCostUsd, 0) : null })),
|
|
1575
|
+
budget: { ...budgetFor(config, saved, null, { checkEstimate: false }), historyVerified: saved.historyVerified !== false },
|
|
1342
1576
|
storageAvailable: saved.storageAvailable !== false,
|
|
1343
1577
|
storageNotice: saved.storageAvailable === false ? '执行记录存储暂不可用;下方费用与学习摘要尚未核验,不能当作零花费或没有反馈。'
|
|
1344
1578
|
: saved.historyVerified === false ? '此前执行记录已损坏或不完整,当前摘要只包含可读记录;不能视为完整费用历史,请从备份恢复后再核验预算。' : '',
|
|
@@ -1499,15 +1733,39 @@ export async function executeOfficialTeamPlan(ctx, config = {}, planned, options
|
|
|
1499
1733
|
reason: '官方 Harness 进程沙箱不可用,不能执行官方 CLI 团队。' }
|
|
1500
1734
|
throwIfAborted(options.signal)
|
|
1501
1735
|
const startedAt = Date.now()
|
|
1502
|
-
const
|
|
1736
|
+
const runId = randomUUID()
|
|
1737
|
+
const makeTeamRecord = (execution, location = {}) => {
|
|
1738
|
+
const run = buildTeamRunRecord({ id: runId, createdAt: startedAt, finishedAt: Date.now(), task: planned.taskText,
|
|
1739
|
+
plan: planned.plan, execution, mode: planned.mode, workspace,
|
|
1740
|
+
preset: planned.plan.preset ?? DEFAULT_ROUTING_PRESET, pricingFor: item => routePricing(planned.routes, item), billingFor: billingForResult,
|
|
1741
|
+
budget: { estimateUsd: budget.estimateUsd, confirmed: options.confirmOverBudget === true } })
|
|
1742
|
+
run.executionEnvironment = executionEnvironment
|
|
1743
|
+
run.cliModels = { ...planned.cliModels }
|
|
1744
|
+
if (planned.mode === 'workspace-write' && location.workspace) run.isolatedWorkspace = location.workspace
|
|
1745
|
+
if (location.baseCommit) run.baseCommit = location.baseCommit
|
|
1746
|
+
return run
|
|
1747
|
+
}
|
|
1748
|
+
const lifecycle = await beginExecutionLifecycle({ state: routerStateStore(), settings: budgetSettings(config),
|
|
1749
|
+
confirmOverBudget: options.confirmOverBudget === true, packageEstimates: packageEstimates(planned.plan, planned.taskText),
|
|
1750
|
+
buildRun: ({ status, results, workspace: location }) => makeTeamRecord({ status, results }, location) })
|
|
1751
|
+
if (!lifecycle.accepted) return { ...common, status: 'paused-budget', budget: pausedExecutionBudget(lifecycle) }
|
|
1752
|
+
let execution
|
|
1753
|
+
try { execution = await (options.runTeam ?? runOfficialTeam)({ plan: planned.plan, task: planned.taskText, workspace,
|
|
1503
1754
|
allowedRoot: options.root ?? workspace, mode: planned.mode, executionEnvironment, installedIds: planned.installed,
|
|
1504
1755
|
cliModels: planned.cliModels, signal: options.signal, sandbox: ctx.sandbox,
|
|
1505
1756
|
onPackageStart: item => reportRunProgress(options, { type: 'package-start', package: item }),
|
|
1506
1757
|
onPackage: item => reportRunProgress(options, { type: 'package-complete', package: item }),
|
|
1507
|
-
onStage: stage => reportRunProgress(options, { type: 'stage', stage })
|
|
1758
|
+
onStage: stage => reportRunProgress(options, { type: 'stage', stage }),
|
|
1759
|
+
onCheckpointStart: item => lifecycle.onCheckpointStart(item), onCheckpointResult: result => lifecycle.onCheckpointResult(result),
|
|
1760
|
+
onWorkspace: value => lifecycle.onWorkspace(value) }) }
|
|
1761
|
+
catch (error) { await stopExecutionLifecycle(lifecycle, error); throw error }
|
|
1508
1762
|
await reportRunProgress(options, { type: 'stage', stage: 'saving' })
|
|
1509
|
-
const run =
|
|
1510
|
-
|
|
1763
|
+
const run = makeTeamRecord(execution, { workspace: execution.workspace, baseCommit: execution.baseCommit })
|
|
1764
|
+
try {
|
|
1765
|
+
await annotateCliQuota(run, config, (execution.results ?? []).filter(item => item.status !== 'succeeded' && item.toolId)
|
|
1766
|
+
.map(item => ({ id: item.id, toolId: item.toolId, text: `${item.error ?? ''}\n${item.outputTail ?? ''}` })))
|
|
1767
|
+
await lifecycle.finish(run)
|
|
1768
|
+
} catch (error) { await lifecycle.interrupt(run); throw error }
|
|
1511
1769
|
return { ...common, status: execution.status, execution, run, runId: run?.id ?? null,
|
|
1512
1770
|
rerunNotice: planned.mode === 'read-only'
|
|
1513
1771
|
? '失败的工作包可用 model_router_rerun_step 单步重跑(只重跑该步及其未完成的下游)。'
|
|
@@ -1605,7 +1863,7 @@ const APPROVAL_TEXT = Object.freeze({
|
|
|
1605
1863
|
*/
|
|
1606
1864
|
export function approvalReasonCodes(name, args = {}, config = {}, { rerunRun = null } = {}) {
|
|
1607
1865
|
const codes = []
|
|
1608
|
-
const runKinds = ['model_router_execute', 'model_router_rerun_step', 'model_router_team_execute', 'model_router_tool_run']
|
|
1866
|
+
const runKinds = ['model_router_execute', 'model_router_rerun_step', 'model_router_team_execute', 'model_router_tool_run', 'model_router_consult']
|
|
1609
1867
|
if (!runKinds.includes(name)) return codes
|
|
1610
1868
|
if ((name === 'model_router_tool_run' || name === 'model_router_team_execute') && args.mode === 'workspace-write') codes.push('workspace-write')
|
|
1611
1869
|
if (name === 'model_router_rerun_step' && rerunRun && rerunSupport(rerunRun).writes) codes.push(rerunRun.kind === 'tool' ? 'workspace-write' : 'rerun-write')
|
|
@@ -1983,6 +2241,7 @@ export function apply(ctx, config = {}) {
|
|
|
1983
2241
|
provider: { type: 'string', description: 'Configured provider id; pair with model.' },
|
|
1984
2242
|
model: { type: 'string', description: 'Configured model id; pair with provider.' },
|
|
1985
2243
|
outputLimit: { type: 'number', description: 'Maximum returned characters, from 500 to 50000.' },
|
|
2244
|
+
confirmOverBudget: { type: 'boolean', description: 'Only after explicit user approval when the API budget is exceeded or unverified.' },
|
|
1986
2245
|
},
|
|
1987
2246
|
output: JSON_OUTPUT,
|
|
1988
2247
|
async execute(args, exec) {
|
|
@@ -1993,7 +2252,7 @@ export function apply(ctx, config = {}) {
|
|
|
1993
2252
|
const route = requested ?? chooseConsultRoute(plan, routes, exec.agent)
|
|
1994
2253
|
const fallback = valueOf(config, 'maxConsultOutputChars', 12_000)
|
|
1995
2254
|
const limit = boundedInteger(args.outputLimit, finiteNumber(fallback, 12_000), 500, 50_000)
|
|
1996
|
-
return jsonValue(await
|
|
2255
|
+
return jsonValue(await executeBudgetedConsult(ctx, route, args.task, config, { limit, signal: exec.signal, confirmOverBudget: args.confirmOverBudget === true }))
|
|
1997
2256
|
},
|
|
1998
2257
|
}))
|
|
1999
2258
|
ctx.commands.register({
|
|
@@ -2213,7 +2472,7 @@ function registerOfficialToolModels(ctx, config, assertInstallPreferences) {
|
|
|
2213
2472
|
return jsonValue({ status: 'paused-budget', budget: { ...planned.budget, paused: true }, estimateUsd: planned.estimate,
|
|
2214
2473
|
notice: `${planned.budget.message} 已暂停,未启动 ${planned.tool.label}。请向用户说明后,以 confirmOverBudget: true 重新调用。` })
|
|
2215
2474
|
}
|
|
2216
|
-
const { result, recorded } = await runPlannedTool(ctx, config, planned, { cwd, root, signal: exec.signal })
|
|
2475
|
+
const { result, recorded } = await runPlannedTool(ctx, config, planned, { cwd, root, signal: exec.signal, confirmOverBudget: args.confirmOverBudget === true })
|
|
2217
2476
|
return jsonValue({ ...result, runId: recorded?.id ?? null, estimateUsd: planned.estimate, budget: planned.budget,
|
|
2218
2477
|
...(!planned.modelId && planned.model ? { modelNotice: result.modelNotice
|
|
2219
2478
|
?? 'Harness 模型 ID 未经此厂商 CLI 验证;本次使用厂商 CLI 已配置的默认模型。' } : {}),
|