@ljwei-stak/dsh-model-router 0.23.1 → 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 +83 -34
- package/.dsh-plugin/index.mjs +281 -51
- 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-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 +13 -0
- package/README.i18n.yaml +2 -2
- package/README.md +25 -17
- package/README.zh.md +24 -16
- package/docs/assets/workbench-budget-reservations.png +0 -0
- package/docs/releases/0.24.0-validation.md +41 -0
- package/package.json +7 -3
package/.dsh-plugin/index.mjs
CHANGED
|
@@ -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
|
}
|
|
@@ -1022,6 +1155,7 @@ function rerunEstimates(run, ids, targetId, override, routes, dataOptions) {
|
|
|
1022
1155
|
|
|
1023
1156
|
export async function rerunRecordedStep(ctx, config = {}, { runId, packageId, provider, model, confirmOverBudget = false, confirmWrite = false, confirmUnsandboxed = false, subscriptionChoice = null, workspace, root, signal, ...options } = {}) {
|
|
1024
1157
|
const { run, saved } = await storedRun(runId)
|
|
1158
|
+
if (saved.budgetReservations?.[run.id]) throw new Error('该运行仍在执行或费用待核对,请先核对原任务与厂商账单,不能直接重跑。')
|
|
1025
1159
|
const target = run.packages.find(item => item.id === text(packageId))
|
|
1026
1160
|
if (!target) throw new Error(`运行记录中没有工作包 ${text(packageId)}`)
|
|
1027
1161
|
const choice = text(subscriptionChoice) || null
|
|
@@ -1068,7 +1202,7 @@ export async function rerunRecordedStep(ctx, config = {}, { runId, packageId, pr
|
|
|
1068
1202
|
const cwd = text(workspace) || run.workspace
|
|
1069
1203
|
if (!cwd || !(await fileExists(cwd))) throw new Error(`原运行的工作区 ${cwd || '(未记录)'} 不可用,无法重跑该步骤;请恢复该目录,或在原工作区的会话中调用 model_router_rerun_step。`)
|
|
1070
1204
|
if (run.kind === 'team') return rerunTeamStep(ctx, { run, target, override, routes, budget,
|
|
1071
|
-
ids: [...retryIds], estimates: estimates.byId, cwd, root: text(root) || cwd, signal, options, config })
|
|
1205
|
+
ids: [...retryIds], estimates: estimates.byId, confirmOverBudget, cwd, root: text(root) || cwd, signal, options, config })
|
|
1072
1206
|
const manualRetries = Object.fromEntries(run.packages.filter(item => retryIds.has(item.id) && item.manualAssignment)
|
|
1073
1207
|
.map(item => [item.id, item.id === target.id && override ? { kind: 'api', ...override } : item.manualAssignment]))
|
|
1074
1208
|
if (Object.keys(manualRetries).length) {
|
|
@@ -1088,7 +1222,18 @@ export async function rerunRecordedStep(ctx, config = {}, { runId, packageId, pr
|
|
|
1088
1222
|
}
|
|
1089
1223
|
const limit = boundedInteger(valueOf(config, 'maxConsultOutputChars', 12_000), 12_000, 500, 50_000)
|
|
1090
1224
|
const retryGate = parseQualityGateJson(valueOf(config, 'qualityGateJson', ''))
|
|
1091
|
-
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({
|
|
1092
1237
|
task: run.task,
|
|
1093
1238
|
packages: run.packages.map(item => ({
|
|
1094
1239
|
id: item.id, name: item.name, objective: item.objective, dependsOn: item.dependsOn,
|
|
@@ -1108,17 +1253,21 @@ export async function rerunRecordedStep(ctx, config = {}, { runId, packageId, pr
|
|
|
1108
1253
|
signal,
|
|
1109
1254
|
...(choice ? { subscriptionChoice: choice } : {}),
|
|
1110
1255
|
...executionHooks(ctx, options),
|
|
1256
|
+
onCheckpointStart: item => lifecycle.onCheckpointStart(item),
|
|
1257
|
+
onCheckpointResult: result => lifecycle.onCheckpointResult(result),
|
|
1111
1258
|
billing: await billingContext(config, options),
|
|
1112
|
-
apiFallback: async ({ route, task: packageTask, signal: callSignal, subscription }) =>
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
}
|
|
1121
|
-
})
|
|
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 }
|
|
1122
1271
|
const awaiting = pausedSteps(execution)
|
|
1123
1272
|
return { run: updated, execution, budget, ...(awaiting.length ? { awaitingConfirmation: awaiting } : {}) }
|
|
1124
1273
|
}
|
|
@@ -1162,7 +1311,7 @@ function teamPlanFromRun(run, ids, override, targetId, estimates) {
|
|
|
1162
1311
|
* Read-only team retry: the failed step plus downstream steps that had not
|
|
1163
1312
|
* succeeded run through the same signed runner; earlier answers are context.
|
|
1164
1313
|
*/
|
|
1165
|
-
async function rerunTeamStep(ctx, { run, target, override, routes, budget, ids, estimates, cwd, root, signal, options, config = {} }) {
|
|
1314
|
+
async function rerunTeamStep(ctx, { run, target, override, routes, budget, ids, estimates, confirmOverBudget, cwd, root, signal, options, config = {} }) {
|
|
1166
1315
|
const mode = run.executionMode === 'workspace-write' ? 'workspace-write' : 'read-only'
|
|
1167
1316
|
const executionEnvironment = teamExecutionEnvironment(run.executionEnvironment, mode)
|
|
1168
1317
|
const plan = teamPlanFromRun(run, ids, override, target.id, estimates)
|
|
@@ -1175,10 +1324,31 @@ async function rerunTeamStep(ctx, { run, target, override, routes, budget, ids,
|
|
|
1175
1324
|
.map(item => ({ id: item.id, name: item.name, finalText: item.answer }))
|
|
1176
1325
|
const runTeam = options.runTeam ?? runOfficialTeam
|
|
1177
1326
|
const installed = Array.isArray(options.installedToolIds) ? options.installedToolIds : await installedToolIds()
|
|
1178
|
-
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,
|
|
1179
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),
|
|
1180
1349
|
...(mode === 'workspace-write' ? { seedFrom: { workspace: run.isolatedWorkspace, baseCommit: run.baseCommit } } : {}),
|
|
1181
|
-
...(options.runtime ? { runtime: options.runtime } : {}) })
|
|
1350
|
+
...(options.runtime ? { runtime: options.runtime } : {}) }) }
|
|
1351
|
+
catch (error) { await stopExecutionLifecycle(lifecycle, error); throw error }
|
|
1182
1352
|
const converted = teamExecutionResults(plan, execution)
|
|
1183
1353
|
if (override) {
|
|
1184
1354
|
const entry = converted.packages.find(item => item.id === target.id)
|
|
@@ -1190,8 +1360,9 @@ async function rerunTeamStep(ctx, { run, target, override, routes, budget, ids,
|
|
|
1190
1360
|
const note = await noteCliQuota(config, result.toolId, `${result.error ?? ''}\n${result.outputTail ?? ''}`)
|
|
1191
1361
|
if (note) quotaNotes.push([result.id, note])
|
|
1192
1362
|
}
|
|
1193
|
-
const updated =
|
|
1194
|
-
|
|
1363
|
+
const updated = buildRetry({ status: execution.status, results: execution.results,
|
|
1364
|
+
workspace: { workspace: execution.workspace, baseCommit: execution.baseCommit } })
|
|
1365
|
+
const current = updated
|
|
1195
1366
|
for (const item of current.packages) {
|
|
1196
1367
|
if (ids.includes(item.id)) item.estimatedCost = estimates.get(item.id) ?? null
|
|
1197
1368
|
}
|
|
@@ -1214,7 +1385,7 @@ async function rerunTeamStep(ctx, { run, target, override, routes, budget, ids,
|
|
|
1214
1385
|
if (cliModels[target.id]) bindings[target.id] = cliModels[target.id]
|
|
1215
1386
|
current.cliModels = bindings
|
|
1216
1387
|
}
|
|
1217
|
-
})
|
|
1388
|
+
try { await lifecycle.finish(updated) } catch (error) { await lifecycle.interrupt(updated); throw error }
|
|
1218
1389
|
return { run: updated, execution: { ...converted, ranIds: ids, raw: execution }, budget }
|
|
1219
1390
|
}
|
|
1220
1391
|
|
|
@@ -1240,6 +1411,7 @@ async function rerunToolRun(ctx, config, { run, confirmOverBudget, confirmWrite,
|
|
|
1240
1411
|
const cwd = text(workspace) || run.workspace
|
|
1241
1412
|
if (!cwd || !(await fileExists(cwd))) throw new Error('原运行的工作区不可用,无法重跑;请在原工作区的会话中重试。')
|
|
1242
1413
|
const { result, recorded } = await runPlannedTool(ctx, config, planned, { cwd, root: text(root) || cwd, signal, rerunOf: run.id,
|
|
1414
|
+
confirmOverBudget,
|
|
1243
1415
|
...(options.runTask ? { runTask: options.runTask } : {}) })
|
|
1244
1416
|
const execution = { status: result.status, packages: recorded?.packages ?? [], ranIds: ['direct'], raw: result }
|
|
1245
1417
|
return { run: recorded ?? run, execution, budget: planned.budget, rerunOf: run.id, newRunId: recorded?.id ?? null }
|
|
@@ -1308,16 +1480,40 @@ export async function planToolRun(ctx, config, args = {}, { signal } = {}) {
|
|
|
1308
1480
|
}
|
|
1309
1481
|
|
|
1310
1482
|
/** Run a planned tool call and record it; `rerunOf` links a rerun to the original run. */
|
|
1311
|
-
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 }) {
|
|
1312
1484
|
const startedAt = Date.now()
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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 }
|
|
1321
1517
|
}
|
|
1322
1518
|
|
|
1323
1519
|
/** Ledger record for model_router_tool_run. */
|
|
@@ -1356,9 +1552,13 @@ export async function rateRecordedResult({ runId, packageId, rating, expectedFin
|
|
|
1356
1552
|
|
|
1357
1553
|
/** Everything the workbench shows: recent runs, spending, budget, learned biases and settings. */
|
|
1358
1554
|
export async function ledgerSummary(config = {}, { limit = 30 } = {}) {
|
|
1555
|
+
await createBudgetReservations({ state: routerStateStore() }).recoverInterruptedExecutions()
|
|
1359
1556
|
const saved = await routingSavedState(config)
|
|
1360
1557
|
const spent = spending(saved.runs, undefined, saved.archivedSpending)
|
|
1361
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 }
|
|
1362
1562
|
return {
|
|
1363
1563
|
runs: saved.runs.slice(-limit).reverse().map(run => ({
|
|
1364
1564
|
...run,
|
|
@@ -1367,7 +1567,12 @@ export async function ledgerSummary(config = {}, { limit = 30 } = {}) {
|
|
|
1367
1567
|
.map(item => ({ ...item, answer: text(item.answer).slice(0, 1_500) })),
|
|
1368
1568
|
})),
|
|
1369
1569
|
spent,
|
|
1370
|
-
|
|
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 },
|
|
1371
1576
|
storageAvailable: saved.storageAvailable !== false,
|
|
1372
1577
|
storageNotice: saved.storageAvailable === false ? '执行记录存储暂不可用;下方费用与学习摘要尚未核验,不能当作零花费或没有反馈。'
|
|
1373
1578
|
: saved.historyVerified === false ? '此前执行记录已损坏或不完整,当前摘要只包含可读记录;不能视为完整费用历史,请从备份恢复后再核验预算。' : '',
|
|
@@ -1528,15 +1733,39 @@ export async function executeOfficialTeamPlan(ctx, config = {}, planned, options
|
|
|
1528
1733
|
reason: '官方 Harness 进程沙箱不可用,不能执行官方 CLI 团队。' }
|
|
1529
1734
|
throwIfAborted(options.signal)
|
|
1530
1735
|
const startedAt = Date.now()
|
|
1531
|
-
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,
|
|
1532
1754
|
allowedRoot: options.root ?? workspace, mode: planned.mode, executionEnvironment, installedIds: planned.installed,
|
|
1533
1755
|
cliModels: planned.cliModels, signal: options.signal, sandbox: ctx.sandbox,
|
|
1534
1756
|
onPackageStart: item => reportRunProgress(options, { type: 'package-start', package: item }),
|
|
1535
1757
|
onPackage: item => reportRunProgress(options, { type: 'package-complete', package: item }),
|
|
1536
|
-
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 }
|
|
1537
1762
|
await reportRunProgress(options, { type: 'stage', stage: 'saving' })
|
|
1538
|
-
const run =
|
|
1539
|
-
|
|
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 }
|
|
1540
1769
|
return { ...common, status: execution.status, execution, run, runId: run?.id ?? null,
|
|
1541
1770
|
rerunNotice: planned.mode === 'read-only'
|
|
1542
1771
|
? '失败的工作包可用 model_router_rerun_step 单步重跑(只重跑该步及其未完成的下游)。'
|
|
@@ -1634,7 +1863,7 @@ const APPROVAL_TEXT = Object.freeze({
|
|
|
1634
1863
|
*/
|
|
1635
1864
|
export function approvalReasonCodes(name, args = {}, config = {}, { rerunRun = null } = {}) {
|
|
1636
1865
|
const codes = []
|
|
1637
|
-
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']
|
|
1638
1867
|
if (!runKinds.includes(name)) return codes
|
|
1639
1868
|
if ((name === 'model_router_tool_run' || name === 'model_router_team_execute') && args.mode === 'workspace-write') codes.push('workspace-write')
|
|
1640
1869
|
if (name === 'model_router_rerun_step' && rerunRun && rerunSupport(rerunRun).writes) codes.push(rerunRun.kind === 'tool' ? 'workspace-write' : 'rerun-write')
|
|
@@ -2012,6 +2241,7 @@ export function apply(ctx, config = {}) {
|
|
|
2012
2241
|
provider: { type: 'string', description: 'Configured provider id; pair with model.' },
|
|
2013
2242
|
model: { type: 'string', description: 'Configured model id; pair with provider.' },
|
|
2014
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.' },
|
|
2015
2245
|
},
|
|
2016
2246
|
output: JSON_OUTPUT,
|
|
2017
2247
|
async execute(args, exec) {
|
|
@@ -2022,7 +2252,7 @@ export function apply(ctx, config = {}) {
|
|
|
2022
2252
|
const route = requested ?? chooseConsultRoute(plan, routes, exec.agent)
|
|
2023
2253
|
const fallback = valueOf(config, 'maxConsultOutputChars', 12_000)
|
|
2024
2254
|
const limit = boundedInteger(args.outputLimit, finiteNumber(fallback, 12_000), 500, 50_000)
|
|
2025
|
-
return jsonValue(await
|
|
2255
|
+
return jsonValue(await executeBudgetedConsult(ctx, route, args.task, config, { limit, signal: exec.signal, confirmOverBudget: args.confirmOverBudget === true }))
|
|
2026
2256
|
},
|
|
2027
2257
|
}))
|
|
2028
2258
|
ctx.commands.register({
|
|
@@ -2242,7 +2472,7 @@ function registerOfficialToolModels(ctx, config, assertInstallPreferences) {
|
|
|
2242
2472
|
return jsonValue({ status: 'paused-budget', budget: { ...planned.budget, paused: true }, estimateUsd: planned.estimate,
|
|
2243
2473
|
notice: `${planned.budget.message} 已暂停,未启动 ${planned.tool.label}。请向用户说明后,以 confirmOverBudget: true 重新调用。` })
|
|
2244
2474
|
}
|
|
2245
|
-
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 })
|
|
2246
2476
|
return jsonValue({ ...result, runId: recorded?.id ?? null, estimateUsd: planned.estimate, budget: planned.budget,
|
|
2247
2477
|
...(!planned.modelId && planned.model ? { modelNotice: result.modelNotice
|
|
2248
2478
|
?? 'Harness 模型 ID 未经此厂商 CLI 验证;本次使用厂商 CLI 已配置的默认模型。' } : {}),
|