@ljwei-stak/dsh-model-router 0.22.1 → 0.23.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 +305 -10
- package/.dsh-plugin/index.mjs +72 -16
- package/.dsh-plugin/official-tools-remote-service.mjs +2 -0
- package/.dsh-plugin/shared/official-team-runtime.mjs +27 -4
- package/.dsh-plugin/shared/official-tools-remote.mjs +11 -0
- package/.dsh-plugin/shared/router-state.mjs +6 -1
- package/.dsh-plugin/shared/run-jobs.mjs +169 -0
- package/.dsh-plugin/shared/task-executors.mjs +28 -6
- package/CHANGELOG.md +14 -0
- package/README.i18n.yaml +2 -2
- package/README.md +59 -17
- package/README.zh.md +59 -17
- package/docs/INSTALLATION_GUIDE.zh.md +12 -12
- package/docs/WORKBENCH_USER_GUIDE.zh.md +5 -1
- package/docs/assets/workbench-run-progress.png +0 -0
- package/docs/releases/0.23.0-validation.md +30 -0
- package/package.json +6 -3
package/.dsh-plugin/index.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
DEFAULT_COOLDOWN_MINUTES, billingOverview, billingPlan, createQuotaTracker, detectQuotaExhaustion, parseQuotaPatterns, vendorKey,
|
|
13
13
|
} from './shared/subscription-billing.mjs'
|
|
14
14
|
import { createRouterState } from './shared/router-state.mjs'
|
|
15
|
+
import { createRunJobs } from './shared/run-jobs.mjs'
|
|
15
16
|
import { applyFeedbackProfile, buildFeedbackProfile } from './shared/adaptive-feedback.mjs'
|
|
16
17
|
import { buildRoutingEvidence, parseRoutingEvidenceJson } from './shared/routing-evidence.mjs'
|
|
17
18
|
import { apiReasoningRequest } from './shared/reasoning-execution.mjs'
|
|
@@ -28,7 +29,7 @@ import { applyManualPackagePlan } from './shared/manual-plan.mjs'
|
|
|
28
29
|
import { parsePackageAssignments, packagePlanKey } from './shared/package-assignments.mjs'
|
|
29
30
|
import { applyModelProfiles, parseModelProfilesJson } from './shared/model-profiles.mjs'
|
|
30
31
|
import { registerOfficialToolsRemote } from './official-tools-remote-service.mjs'
|
|
31
|
-
import { parseRunRequest } from './shared/official-tools-remote.mjs'
|
|
32
|
+
import { parseRunRequest, parseRunJobRequest } from './shared/official-tools-remote.mjs'
|
|
32
33
|
import { createTerminalManager } from './shared/cli-terminal.mjs'
|
|
33
34
|
import {
|
|
34
35
|
parseTerminalRead,
|
|
@@ -682,6 +683,7 @@ export async function reviewRunPackages(ctx, run, routes, config, { signal, rand
|
|
|
682
683
|
const reviewerQuality = routeQuality(reviewer)
|
|
683
684
|
const reviews = []
|
|
684
685
|
for (const item of run.packages) {
|
|
686
|
+
if (signal?.aborted) break
|
|
685
687
|
if (!item.ok || !text(item.answer)) continue
|
|
686
688
|
if (item.provider === reviewer.provider && item.model === reviewer.model) continue
|
|
687
689
|
const own = routeQuality(routes.find(route => route.provider === item.provider && route.model === item.model))
|
|
@@ -708,16 +710,26 @@ export async function reviewRunPackages(ctx, run, routes, config, { signal, rand
|
|
|
708
710
|
run.reviews.push(entry)
|
|
709
711
|
reviews.push({ ...entry, score })
|
|
710
712
|
} catch (error) {
|
|
711
|
-
if (signal?.aborted)
|
|
713
|
+
if (signal?.aborted) break
|
|
712
714
|
item.review = { provider: reviewer.provider, model: reviewer.model, score: null, summary: `审阅失败:${errorText(error).slice(0, 200)}`, at: Date.now() }
|
|
713
715
|
}
|
|
714
716
|
}
|
|
715
717
|
return reviews
|
|
716
718
|
}
|
|
717
719
|
|
|
720
|
+
async function reportRunProgress(options, event) {
|
|
721
|
+
try { if (typeof options.onProgress === 'function') await options.onProgress(event) }
|
|
722
|
+
catch { /* Observing progress must not discard results or incurred spend. */ }
|
|
723
|
+
}
|
|
724
|
+
|
|
718
725
|
function executionHooks(ctx, options) {
|
|
719
726
|
return {
|
|
720
727
|
skipOfficial: options.skipOfficial ?? (request => healthCache.skipReason(request)),
|
|
728
|
+
onStage: stage => reportRunProgress(options, { type: 'stage', stage }),
|
|
729
|
+
onPackageStart: async item => {
|
|
730
|
+
await reportRunProgress(options, { type: 'package-start', package: item })
|
|
731
|
+
if (typeof options.onPackageStart === 'function') await options.onPackageStart(item)
|
|
732
|
+
},
|
|
721
733
|
onPackage: async result => {
|
|
722
734
|
// A CLI that reports "not logged in" is marked; with a subscription the router then asks instead of using the API key.
|
|
723
735
|
const failed = result?.toolId && (result.fallback?.loginRequired ? result.fallback.error : result.pause?.loginRequired && result.pause.kind !== 'subscription-login' ? result.pause.detail : null)
|
|
@@ -729,6 +741,7 @@ function executionHooks(ctx, options) {
|
|
|
729
741
|
healthCache.clearLoggedOut(result.toolId)
|
|
730
742
|
try { await routerStateStore().saveAuthFailure(result.toolId, null) } catch { /* the cache still serves this process */ }
|
|
731
743
|
}
|
|
744
|
+
await reportRunProgress(options, { type: 'package-complete', package: result })
|
|
732
745
|
if (typeof options.onPackage === 'function') await options.onPackage(result)
|
|
733
746
|
},
|
|
734
747
|
credentialsFor: route => credentialsForRoute(ctx, route),
|
|
@@ -857,7 +870,10 @@ export async function executeConfiguredAssignment(ctx, task, config = {}, option
|
|
|
857
870
|
}
|
|
858
871
|
}
|
|
859
872
|
const finished = []
|
|
860
|
-
const hooks = executionHooks(ctx, { ...options, onPackage: result => {
|
|
873
|
+
const hooks = executionHooks(ctx, { ...options, onPackage: async result => {
|
|
874
|
+
finished.push(result)
|
|
875
|
+
if (typeof options.onPackage === 'function') await options.onPackage(result)
|
|
876
|
+
} })
|
|
861
877
|
const billing = await billingContext(config, options)
|
|
862
878
|
const limit = boundedInteger(valueOf(config, 'maxConsultOutputChars', 12_000), 12_000, 500, 50_000)
|
|
863
879
|
const startedAt = Date.now()
|
|
@@ -934,7 +950,10 @@ export async function executeConfiguredAssignment(ctx, task, config = {}, option
|
|
|
934
950
|
billingFor: billingForResult,
|
|
935
951
|
budget: { estimateUsd: budget.estimateUsd, exceeded: budget.exceeded, downgraded: budget.downgraded === true, confirmed: budget.exceeded ? true : undefined },
|
|
936
952
|
})
|
|
953
|
+
await reportRunProgress(options, { type: 'stage', stage: 'reviewing' })
|
|
937
954
|
const reviews = await reviewRunPackages(ctx, run, routes, config, { signal: options.signal, random: options.random })
|
|
955
|
+
if (options.signal?.aborted) { run.status = 'cancelled'; execution.status = 'cancelled' }
|
|
956
|
+
await reportRunProgress(options, { type: 'stage', stage: 'saving' })
|
|
938
957
|
let stored = true
|
|
939
958
|
try { await routerStateStore().appendRun(run) } catch { stored = false }
|
|
940
959
|
const awaiting = pausedSteps(execution)
|
|
@@ -1482,7 +1501,11 @@ export async function executeOfficialTeamPlan(ctx, config = {}, planned, options
|
|
|
1482
1501
|
const startedAt = Date.now()
|
|
1483
1502
|
const execution = await (options.runTeam ?? runOfficialTeam)({ plan: planned.plan, task: planned.taskText, workspace,
|
|
1484
1503
|
allowedRoot: options.root ?? workspace, mode: planned.mode, executionEnvironment, installedIds: planned.installed,
|
|
1485
|
-
cliModels: planned.cliModels, signal: options.signal, sandbox: ctx.sandbox
|
|
1504
|
+
cliModels: planned.cliModels, signal: options.signal, sandbox: ctx.sandbox,
|
|
1505
|
+
onPackageStart: item => reportRunProgress(options, { type: 'package-start', package: item }),
|
|
1506
|
+
onPackage: item => reportRunProgress(options, { type: 'package-complete', package: item }),
|
|
1507
|
+
onStage: stage => reportRunProgress(options, { type: 'stage', stage }) })
|
|
1508
|
+
await reportRunProgress(options, { type: 'stage', stage: 'saving' })
|
|
1486
1509
|
const run = await recordTeamRun({ task: planned.taskText, plan: planned.plan, execution, mode: planned.mode, executionEnvironment, workspace,
|
|
1487
1510
|
routes: planned.routes, cliModels: planned.cliModels, budget, startedAt, config })
|
|
1488
1511
|
return { ...common, status: execution.status, execution, run, runId: run?.id ?? null,
|
|
@@ -1720,15 +1743,26 @@ export async function previewWorkbenchRun(ctx, config, request = {}, options = {
|
|
|
1720
1743
|
/** Execute from the workbench; refuses until every current reason is in confirmedReasons. */
|
|
1721
1744
|
export async function startWorkbenchRun(ctx, config, supplied = {}, options = {}) {
|
|
1722
1745
|
const prepared = await prepareWorkbenchPreview(ctx, config, supplied, options)
|
|
1723
|
-
|
|
1746
|
+
return refusedWorkbenchStart(prepared) ?? executePreparedWorkbenchRun(ctx, config, prepared, options)
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
function refusedWorkbenchStart({ preview, request }) {
|
|
1724
1750
|
if (preview.status === 'blocked') return preview
|
|
1725
|
-
const confirmed = new Set(
|
|
1751
|
+
const confirmed = new Set(request.confirmedReasons ?? [])
|
|
1726
1752
|
const missing = preview.reasons.filter(item => !confirmed.has(item.code))
|
|
1727
|
-
|
|
1753
|
+
return missing.length ? { ...preview, status: 'needs-confirmation', reasons: preview.reasons,
|
|
1754
|
+
missing: missing.map(item => item.code) } : null
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
async function executePreparedWorkbenchRun(ctx, config, prepared, options = {}) {
|
|
1758
|
+
const { preview, request } = prepared
|
|
1759
|
+
const confirmed = new Set(Array.isArray(request.confirmedReasons) ? request.confirmedReasons : [])
|
|
1760
|
+
throwIfAborted(options.signal)
|
|
1761
|
+
await reportRunProgress(options, { type: 'stage', stage: 'executing' })
|
|
1728
1762
|
if (request.executor === 'official-team') {
|
|
1729
1763
|
const result = await (options.executeOfficialTeam ?? executeOfficialTeamPlan)(ctx, config, prepared.planned, {
|
|
1730
|
-
workspace: preview.workspace, root: preview.workspace, signal: options.signal,
|
|
1731
1764
|
confirmOverBudget: confirmed.has('over-budget'), confirmUnsandboxed: confirmed.has('unsandboxed'), ...(options.teamOptions ?? {}),
|
|
1765
|
+
workspace: preview.workspace, root: preview.workspace, signal: options.signal, onProgress: options.onProgress,
|
|
1732
1766
|
})
|
|
1733
1767
|
return { status: result.status, runId: result.runId ?? null, workspace: preview.workspace,
|
|
1734
1768
|
executor: 'official-team', executionMode: request.executionMode, executionEnvironment: preview.executionEnvironment, decision: preview.decision,
|
|
@@ -1740,8 +1774,8 @@ export async function startWorkbenchRun(ctx, config, supplied = {}, options = {}
|
|
|
1740
1774
|
provider: request.provider, model: request.model, planMode: request.planMode, preset: request.preset,
|
|
1741
1775
|
...manualRequestOptions(request),
|
|
1742
1776
|
budgetUsd: finiteNumber(request.budgetUsd, finiteNumber(configuredBudget, 0)),
|
|
1743
|
-
confirmOverBudget: confirmed.has('over-budget'), workspace: preview.workspace,
|
|
1744
|
-
preparedAssignment: prepared.planned,
|
|
1777
|
+
confirmOverBudget: confirmed.has('over-budget'), workspace: preview.workspace, ...options.planOptions,
|
|
1778
|
+
preparedAssignment: prepared.planned, signal: options.signal, onProgress: options.onProgress,
|
|
1745
1779
|
})
|
|
1746
1780
|
const awaiting = result.awaitingConfirmation ?? []
|
|
1747
1781
|
return {
|
|
@@ -1751,6 +1785,25 @@ export async function startWorkbenchRun(ctx, config, supplied = {}, options = {}
|
|
|
1751
1785
|
}
|
|
1752
1786
|
}
|
|
1753
1787
|
|
|
1788
|
+
/** Desktop starts an observable job after fresh preflight and confirmation checks. */
|
|
1789
|
+
export async function launchWorkbenchRun(ctx, config, supplied = {}, { runJobs, ...options } = {}) {
|
|
1790
|
+
if (!runJobs) throw new Error('工作台任务服务不可用,请重启 Harness 后重试。')
|
|
1791
|
+
const prepared = await prepareWorkbenchPreview(ctx, config, supplied, options)
|
|
1792
|
+
const refused = refusedWorkbenchStart(prepared)
|
|
1793
|
+
if (refused) return refused
|
|
1794
|
+
return runJobs.start({
|
|
1795
|
+
packages: prepared.preview.decision?.packages ?? [],
|
|
1796
|
+
run: async ({ signal, onProgress }) => {
|
|
1797
|
+
try { return await executePreparedWorkbenchRun(ctx, config, prepared, { ...options, signal, onProgress }) }
|
|
1798
|
+
catch (error) {
|
|
1799
|
+
if (error?.runId) return { status: signal.aborted ? 'cancelled' : 'failed', runId: error.runId,
|
|
1800
|
+
workspace: prepared.preview.workspace, notice: '执行未完成,已产生的结果与费用请查看执行记录。' }
|
|
1801
|
+
throw error
|
|
1802
|
+
}
|
|
1803
|
+
},
|
|
1804
|
+
})
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1754
1807
|
/** Host operations behind the workbench RPC; the client never supplies commands or paths. */
|
|
1755
1808
|
/**
|
|
1756
1809
|
* One terminal manager per Host plugin fiber. Sessions run outside the
|
|
@@ -1786,7 +1839,7 @@ function terminalServices(terminals, assertPreferences = () => {}) {
|
|
|
1786
1839
|
}
|
|
1787
1840
|
}
|
|
1788
1841
|
|
|
1789
|
-
export function routerRemoteServices(ctx, config, { terminals = null, assertPreferences } = {}) {
|
|
1842
|
+
export function routerRemoteServices(ctx, config, { terminals = null, assertPreferences, runJobs = createRunJobs(), runOptions = {} } = {}) {
|
|
1790
1843
|
return {
|
|
1791
1844
|
...terminalServices(terminals, assertPreferences),
|
|
1792
1845
|
health: async fresh => {
|
|
@@ -1804,8 +1857,10 @@ export function routerRemoteServices(ctx, config, { terminals = null, assertPref
|
|
|
1804
1857
|
...(request?.subscriptionChoice ? { subscriptionChoice: request.subscriptionChoice } : {}),
|
|
1805
1858
|
}),
|
|
1806
1859
|
boundaries: () => securityBoundaries(ctx, config),
|
|
1807
|
-
previewRun: request => previewWorkbenchRun(ctx, config, request),
|
|
1808
|
-
startRun: request =>
|
|
1860
|
+
previewRun: request => previewWorkbenchRun(ctx, config, request, runOptions),
|
|
1861
|
+
startRun: request => launchWorkbenchRun(ctx, config, request, { ...runOptions, runJobs }),
|
|
1862
|
+
runStatus: request => runJobs.status(parseRunJobRequest(request).jobId),
|
|
1863
|
+
cancelRun: request => runJobs.cancel(parseRunJobRequest(request).jobId),
|
|
1809
1864
|
}
|
|
1810
1865
|
}
|
|
1811
1866
|
|
|
@@ -1835,14 +1890,15 @@ export function apply(ctx, config = {}) {
|
|
|
1835
1890
|
// supply only the model/command services and do not expose the Desktop RPC.
|
|
1836
1891
|
if (ctx.typert) {
|
|
1837
1892
|
const terminals = createRouterTerminals()
|
|
1893
|
+
const runJobs = createRunJobs()
|
|
1838
1894
|
// Plugin unload or patch reload kills every interactive session.
|
|
1839
1895
|
ctx.effect(() => {
|
|
1840
|
-
const onExit = () => { terminals.disposeAll('dispose') }
|
|
1896
|
+
const onExit = () => { terminals.disposeAll('dispose'); runJobs.dispose() }
|
|
1841
1897
|
process.once('exit', onExit)
|
|
1842
|
-
return () => { process.off('exit', onExit); terminals.disposeAll('dispose') }
|
|
1898
|
+
return () => { process.off('exit', onExit); terminals.disposeAll('dispose'); runJobs.dispose() }
|
|
1843
1899
|
}, 'model-router-galgame: interactive terminals')
|
|
1844
1900
|
registerOfficialToolsRemote(ctx, {
|
|
1845
|
-
...routerRemoteServices(ctx, config, { terminals, assertPreferences: assertInstallPreferences }),
|
|
1901
|
+
...routerRemoteServices(ctx, config, { terminals, runJobs, assertPreferences: assertInstallPreferences }),
|
|
1846
1902
|
installSettingsError: () => installPreferenceError,
|
|
1847
1903
|
installRequestedDirectory: () => valueOf(config, 'toolInstallDir', ''),
|
|
1848
1904
|
zcodeRequestedDirectory: () => valueOf(config, 'zcodeInstallDir', ''),
|
|
@@ -149,6 +149,8 @@ export class OfficialToolsRemoteService extends TypertRemoteService {
|
|
|
149
149
|
|
|
150
150
|
/** Execute a previewed run once every listed reason was confirmed. */
|
|
151
151
|
startRun(request) { return settled(() => (this.services.startRun ?? unavailable)(request)) }
|
|
152
|
+
runStatus(request) { return settled(() => (this.services.runStatus ?? unavailable)(request)) }
|
|
153
|
+
cancelRun(request) { return settled(() => (this.services.cancelRun ?? unavailable)(request)) }
|
|
152
154
|
|
|
153
155
|
/** Terminal backend (PTY or pipe fallback), limits and live sessions. */
|
|
154
156
|
terminalInfo() { return settled(() => (this.services.terminalInfo ?? unavailable)()) }
|
|
@@ -11,7 +11,7 @@ import { tmpdir } from 'node:os'
|
|
|
11
11
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
12
12
|
import { runOfficialTool, officialToolExecutionCapabilities, officialToolReadiness } from './official-tool-executor.mjs'
|
|
13
13
|
import { toolForProvider } from './official-tool-registry.mjs'
|
|
14
|
-
import { redactDiagnostic, verifiedDiagnostic } from './task-executors.mjs'
|
|
14
|
+
import { observeExecutionProgress, redactDiagnostic, verifiedDiagnostic } from './task-executors.mjs'
|
|
15
15
|
import { unsupportedCliReasoning } from './reasoning-execution.mjs'
|
|
16
16
|
|
|
17
17
|
const MAX_GIT_OUTPUT = 64_000_000
|
|
@@ -96,6 +96,8 @@ export async function sessionWorkspace(ctx, exec) {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
function git(args, { cwd, input, signal, timeoutMs = GIT_TIMEOUT_MS } = {}) {
|
|
99
|
+
// In particular, never start git apply with an already cancelled signal.
|
|
100
|
+
if (signal?.aborted) return Promise.resolve({ ok: false, code: null, output: Buffer.alloc(0), cancelled: true })
|
|
99
101
|
return new Promise(resolveResult => {
|
|
100
102
|
let child
|
|
101
103
|
try {
|
|
@@ -123,6 +125,7 @@ function git(args, { cwd, input, signal, timeoutMs = GIT_TIMEOUT_MS } = {}) {
|
|
|
123
125
|
const timer = setTimeout(() => { timedOut = true; child.kill('SIGTERM') }, timeoutMs)
|
|
124
126
|
child.on('error', error => finish(null, error))
|
|
125
127
|
child.on('close', code => finish(code))
|
|
128
|
+
if (signal?.aborted) onAbort()
|
|
126
129
|
if (input === undefined) child.stdin.end()
|
|
127
130
|
else child.stdin.end(input)
|
|
128
131
|
let finished = false
|
|
@@ -412,7 +415,8 @@ async function seedWorktree(isolated, seedFrom, signal) {
|
|
|
412
415
|
}
|
|
413
416
|
|
|
414
417
|
/** Sequential execution preserves DAG dependencies and avoids edit conflicts. */
|
|
415
|
-
export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode = 'read-only', executionEnvironment = 'harness', installedIds = [], cliModels, signal, sandbox, runtime, previous = [], onlyIds = null, seedFrom = null }) {
|
|
418
|
+
export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode = 'read-only', executionEnvironment = 'harness', installedIds = [], cliModels, signal, sandbox, runtime, previous = [], onlyIds = null, seedFrom = null, onPackageStart, onPackage, onStage }) {
|
|
419
|
+
await observeExecutionProgress(onStage, 'preparing')
|
|
416
420
|
if (!['harness', 'cli-native'].includes(executionEnvironment)
|
|
417
421
|
|| (executionEnvironment === 'cli-native' && mode !== 'workspace-write')) throw new TypeError('invalid team CLI execution environment')
|
|
418
422
|
// A retry runs only `onlyIds`; earlier results feed dependency context. An
|
|
@@ -431,6 +435,7 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
|
|
|
431
435
|
if (mode !== 'read-only' && mode !== 'workspace-write') throw new TypeError('team mode must be read-only or workspace-write')
|
|
432
436
|
const configuredCliModels = checkedTeamCliModels(cliModels, assigned)
|
|
433
437
|
const source = await approvedSource(workspace, allowedRoot)
|
|
438
|
+
if (signal?.aborted) return { status: 'cancelled', workspace: source, results: [] }
|
|
434
439
|
if (executionEnvironment === 'harness' && typeof sandbox?.confine !== 'function') return { status: 'blocked', blocking: [{ reason: '官方 Harness 进程沙箱不可用。' }], results: [] }
|
|
435
440
|
// The Host passes no runtime override. Tests can inject inert CLI adapters to
|
|
436
441
|
// exercise assignment, stopping, and Git integration without provider accounts.
|
|
@@ -440,6 +445,7 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
|
|
|
440
445
|
const unready = readiness.filter(item => !item.ready || !(item.readyModes ?? item.modes ?? [mode]).includes(mode))
|
|
441
446
|
if (unready.length > 0) return { status: 'blocked', blocking: unready.map(item => ({ toolId: item.id,
|
|
442
447
|
reason: item.reason ?? `官方 CLI 尚未就绪,或不支持 ${mode} 模式。` })), results: [] }
|
|
448
|
+
if (signal?.aborted) return { status: 'cancelled', workspace: source, results: [] }
|
|
443
449
|
const isolated = mode === 'workspace-write' ? await isolatedWorktree(source, signal, allowedRoot) : null
|
|
444
450
|
const runWorkspace = isolated?.workspace ?? source
|
|
445
451
|
let seeded = null
|
|
@@ -451,13 +457,18 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
|
|
|
451
457
|
}
|
|
452
458
|
}
|
|
453
459
|
const completed = []
|
|
460
|
+
const cancelledRun = () => ({ status: 'cancelled', workspace: runWorkspace, results: completed,
|
|
461
|
+
...(isolated ? { baseCommit: isolated.baseCommit } : {}), ...(seeded ? { seeded } : {}) })
|
|
454
462
|
const context = retry ? (Array.isArray(previous) ? previous : []).filter(item => item?.id && typeof item.finalText === 'string') : []
|
|
463
|
+
if (!signal?.aborted) await observeExecutionProgress(onStage, 'executing')
|
|
455
464
|
for (const item of assigned) {
|
|
456
|
-
if (signal?.aborted) return
|
|
457
|
-
...(isolated ? { baseCommit: isolated.baseCommit } : {}), ...(seeded ? { seeded } : {}) }
|
|
465
|
+
if (signal?.aborted) return cancelledRun()
|
|
458
466
|
const cliModel = teamCliModel(item, configuredCliModels)
|
|
459
467
|
const prompt = packagePrompt(task, item, [...context, ...completed])
|
|
460
468
|
const startedAt = Date.now()
|
|
469
|
+
await observeExecutionProgress(onPackageStart, { ...item, provider: item.recommendedProvider,
|
|
470
|
+
model: item.recommendedModel, requestedCliModel: cliModel, startedAt })
|
|
471
|
+
if (signal?.aborted) return cancelledRun()
|
|
461
472
|
let result
|
|
462
473
|
try {
|
|
463
474
|
result = await runTool({
|
|
@@ -507,11 +518,15 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
|
|
|
507
518
|
error: modelMismatch ? '官方 CLI 实际模型与指定模型不一致。'
|
|
508
519
|
: (result.error || result.reason) ? executionDiagnostic(result.error || result.reason) : null,
|
|
509
520
|
outputTail: result.status === 'succeeded' ? null : executionDiagnostic(verifiedDiagnostic(result)) || null })
|
|
521
|
+
await observeExecutionProgress(onPackage, completed.at(-1))
|
|
522
|
+
if (signal?.aborted) return cancelledRun()
|
|
510
523
|
if (packageStatus !== 'succeeded') return { status: packageStatus === 'cancelled' ? 'cancelled' : 'incomplete', workspace: runWorkspace, results: completed,
|
|
511
524
|
...(isolated ? { baseCommit: isolated.baseCommit } : {}), ...(seeded ? { seeded } : {}) }
|
|
512
525
|
}
|
|
513
526
|
if (!isolated) return { status: 'cli-completed', workspace: runWorkspace, results: completed,
|
|
514
527
|
notice: '只读模式已收集各工作包结果;模型输出仍需人工验收。' }
|
|
528
|
+
await observeExecutionProgress(onStage, 'integrating')
|
|
529
|
+
if (signal?.aborted) return cancelledRun()
|
|
515
530
|
try {
|
|
516
531
|
const integration = await integrateWorktree(source, runWorkspace, isolated.baseCommit, signal)
|
|
517
532
|
return { status: integration.ignoredArtifacts > 0 ? 'integration-pending' : 'cli-completed',
|
|
@@ -520,6 +535,8 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
|
|
|
520
535
|
? '受 Git 管理的源文件已整合;忽略产物保留在独立工作区,仍需核对和补交。'
|
|
521
536
|
: '各 CLI 进程成功结束且可用补丁已整合;文件内容仍需按任务要求验收。' }
|
|
522
537
|
} catch (error) {
|
|
538
|
+
if (signal?.aborted) return { ...cancelledRun(), error: String(error?.message ?? error),
|
|
539
|
+
notice: '执行已取消;独立工作区及工作包结果已保留。' }
|
|
523
540
|
return { status: 'integration-pending', workspace: runWorkspace, results: completed, baseCommit: isolated.baseCommit,
|
|
524
541
|
...(seeded ? { seeded } : {}),
|
|
525
542
|
error: String(error?.message ?? error), notice: '独立工作区已保留;原工作区未自动整合。' }
|
|
@@ -529,12 +546,14 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
|
|
|
529
546
|
/** One editable CLI task also uses the same isolated workspace and merge gate. */
|
|
530
547
|
export async function runOfficialTask({ toolId, task, workspace, allowedRoot, modelId, mode = 'read-only', signal, sandbox, runtime }) {
|
|
531
548
|
const source = await approvedSource(workspace, allowedRoot)
|
|
549
|
+
if (signal?.aborted) return { toolId, status: 'cancelled', notStarted: true, finalText: '' }
|
|
532
550
|
if (typeof sandbox?.confine !== 'function') return { toolId, status: 'unsupported', reason: '官方 Harness 进程沙箱不可用。' }
|
|
533
551
|
// The Host supplies no runtime override; offline tests use inert adapters.
|
|
534
552
|
const readiness = await (runtime?.readiness ?? officialToolReadiness)(toolId, source)
|
|
535
553
|
const readyModes = readiness.readyModes ?? readiness.modes ?? []
|
|
536
554
|
if (!readiness.ready || !readyModes.includes(mode)) return { toolId, status: 'unsupported',
|
|
537
555
|
reason: readiness.reason ?? readiness.modeReasons?.[mode] ?? `官方 CLI 尚未就绪,或不支持 ${mode} 模式。` }
|
|
556
|
+
if (signal?.aborted) return { toolId, status: 'cancelled', notStarted: true, finalText: '' }
|
|
538
557
|
const isolated = mode === 'workspace-write' ? await isolatedWorktree(source, signal, allowedRoot) : null
|
|
539
558
|
let result
|
|
540
559
|
try {
|
|
@@ -545,6 +564,8 @@ export async function runOfficialTask({ toolId, task, workspace, allowedRoot, mo
|
|
|
545
564
|
result = executionException(error, signal)
|
|
546
565
|
}
|
|
547
566
|
result = withFailureDiagnostic(result)
|
|
567
|
+
if (signal?.aborted) return { ...result, status: 'cancelled',
|
|
568
|
+
...(isolated ? { isolatedWorkspace: isolated.workspace, baseCommit: isolated.baseCommit } : {}) }
|
|
548
569
|
if (!isolated || result.status !== 'succeeded') return { ...result, ...(isolated ? { isolatedWorkspace: isolated.workspace, baseCommit: isolated.baseCommit } : {}) }
|
|
549
570
|
try {
|
|
550
571
|
const integration = await integrateWorktree(source, isolated.workspace, isolated.baseCommit, signal)
|
|
@@ -552,6 +573,8 @@ export async function runOfficialTask({ toolId, task, workspace, allowedRoot, mo
|
|
|
552
573
|
...(integration.ignoredArtifacts > 0 ? { status: 'integration-pending' } : {}),
|
|
553
574
|
isolatedWorkspace: isolated.workspace, integration }
|
|
554
575
|
} catch (error) {
|
|
576
|
+
if (signal?.aborted) return { ...result, status: 'cancelled', isolatedWorkspace: isolated.workspace,
|
|
577
|
+
baseCommit: isolated.baseCommit, integrationError: String(error?.message ?? error) }
|
|
555
578
|
return { ...result, status: 'integration-pending', isolatedWorkspace: isolated.workspace,
|
|
556
579
|
integrationError: String(error?.message ?? error) }
|
|
557
580
|
}
|
|
@@ -104,6 +104,15 @@ const idText = (value, subject) => {
|
|
|
104
104
|
return value
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/** A status/cancel request can only address an existing Host-owned run. */
|
|
108
|
+
export function parseRunJobRequest(value) {
|
|
109
|
+
const request = plainObject(value, 'run job request')
|
|
110
|
+
if (Object.keys(request).some(key => key !== 'jobId')) throw new TypeError('run job request only accepts jobId')
|
|
111
|
+
return { jobId: idText(request.jobId, 'jobId') }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const runJobRequestCodec = strictCodec(`${OFFICIAL_TOOLS_REMOTE_PACKAGE}#RunJobRequest`, parseRunJobRequest)
|
|
115
|
+
|
|
107
116
|
const optionalRouteText = (value, subject) => {
|
|
108
117
|
if (value === undefined || value === null || value === '') return undefined
|
|
109
118
|
if (typeof value !== 'string' || value.length > 240 || value.includes('\0')) throw new TypeError(`${subject} is invalid`)
|
|
@@ -231,6 +240,8 @@ export const OFFICIAL_TOOLS_REMOTE_DESCRIPTORS = Object.freeze([
|
|
|
231
240
|
// Workbench "start a run": plan preview with cost estimate and confirmation reasons, then execute.
|
|
232
241
|
descriptor('previewRun', [jsonParameter('request', runRequestCodec)], anyObjectCodec('RunPreview')),
|
|
233
242
|
descriptor('startRun', [jsonParameter('request', runRequestCodec)], anyObjectCodec('RunStarted')),
|
|
243
|
+
descriptor('runStatus', [jsonParameter('request', runJobRequestCodec)], anyObjectCodec('RunJobStatus')),
|
|
244
|
+
descriptor('cancelRun', [jsonParameter('request', runJobRequestCodec)], anyObjectCodec('RunJobStatus')),
|
|
234
245
|
// Workbench "官方工具终端": interactive shell / fixed official CLI sessions.
|
|
235
246
|
// Output streams through long-poll reads; input and resize are unary calls.
|
|
236
247
|
descriptor('terminalInfo', [], anyObjectCodec('TerminalInfo')),
|
|
@@ -269,7 +269,12 @@ export function createRouterState({ file = defaultStatePath(), maxRuns = MAX_RUN
|
|
|
269
269
|
try {
|
|
270
270
|
cache = null
|
|
271
271
|
const { state } = await readFresh({ recoverCorrupt: true })
|
|
272
|
-
if (pendingNotices.length)
|
|
272
|
+
if (pendingNotices.length) {
|
|
273
|
+
state.notices.push(...pendingNotices)
|
|
274
|
+
// Recovery may already have moved the damaged file before a failed
|
|
275
|
+
// update. Its pending notice must carry the missing-history marker.
|
|
276
|
+
state.historyIncomplete ||= pendingNotices.some(notice => notice.kind === 'state-corrupt')
|
|
277
|
+
}
|
|
273
278
|
const result = await change(state)
|
|
274
279
|
const returned = result === undefined ? undefined : structuredClone(result)
|
|
275
280
|
retainRuns(state, historyLimit)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory Host workbench jobs. Only package identity and state enter progress;
|
|
3
|
+
* answers and the rest of the workbench response stay in the final result.
|
|
4
|
+
*/
|
|
5
|
+
import { randomUUID } from 'node:crypto'
|
|
6
|
+
|
|
7
|
+
const ACTIVE = new Set(['running', 'cancelling'])
|
|
8
|
+
const PACKAGE_DONE = new Set(['succeeded', 'failed', 'cancelled', 'blocked', 'skipped'])
|
|
9
|
+
const FAILURE = 'Run failed. Please try again.'
|
|
10
|
+
|
|
11
|
+
const copy = value => JSON.parse(JSON.stringify(value))
|
|
12
|
+
const label = (value, fallback = '') => typeof value === 'string' ? value : fallback
|
|
13
|
+
|
|
14
|
+
function positiveLimit(value, name) {
|
|
15
|
+
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`${name} must be a positive integer.`)
|
|
16
|
+
return value
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function completionStatus(item) {
|
|
20
|
+
if (item.cancelled === true || item.status === 'cancelled') return 'cancelled'
|
|
21
|
+
if (item.blocked === true || item.status === 'blocked') return 'blocked'
|
|
22
|
+
if (item.notStarted === true || item.status === 'skipped') return 'skipped'
|
|
23
|
+
if (item.status === 'succeeded' || item.status === 'failed') return item.status
|
|
24
|
+
return item.ok === true ? 'succeeded' : 'failed'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* start() returns before invoking run(). Cancellation requests abort the same
|
|
29
|
+
* signal given to run(), but the job remains active until run actually settles.
|
|
30
|
+
* maxJobs bounds all retained jobs, and only terminal jobs may be evicted.
|
|
31
|
+
*/
|
|
32
|
+
export function createRunJobs({ now = Date.now, maxJobs = 50, maxActive = 4 } = {}) {
|
|
33
|
+
if (typeof now !== 'function') throw new TypeError('now must be a function.')
|
|
34
|
+
positiveLimit(maxJobs, 'maxJobs')
|
|
35
|
+
positiveLimit(maxActive, 'maxActive')
|
|
36
|
+
const jobs = new Map()
|
|
37
|
+
let disposed = false
|
|
38
|
+
|
|
39
|
+
// A clock supplied by a Host must not turn a settled worker into an unhandled
|
|
40
|
+
// rejection or make a snapshot impossible to send over JSON.
|
|
41
|
+
const timestamp = () => {
|
|
42
|
+
try {
|
|
43
|
+
const value = now()
|
|
44
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
45
|
+
} catch { /* retain a usable timestamp if the injected clock fails */ }
|
|
46
|
+
return Date.now()
|
|
47
|
+
}
|
|
48
|
+
const snapshot = job => copy(job.snapshot)
|
|
49
|
+
const find = jobId => {
|
|
50
|
+
const job = jobs.get(jobId)
|
|
51
|
+
if (!job) throw new Error('Unknown run job id.')
|
|
52
|
+
return job
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function refreshProgress(job) {
|
|
56
|
+
const progress = job.snapshot.progress
|
|
57
|
+
progress.total = progress.packages.length
|
|
58
|
+
progress.completed = progress.packages.filter(item => PACKAGE_DONE.has(item.status)).length
|
|
59
|
+
const current = progress.packages.findLast(item => item.status === 'running')
|
|
60
|
+
progress.current = current ? { id: current.id, name: current.name } : null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function packageFor(job, value) {
|
|
64
|
+
if (!value || typeof value !== 'object') return null
|
|
65
|
+
const id = label(value.id)
|
|
66
|
+
if (!id) return null
|
|
67
|
+
let item = job.snapshot.progress.packages.find(item => item.id === id)
|
|
68
|
+
if (!item) {
|
|
69
|
+
item = { id, name: label(value.name, id), status: 'pending' }
|
|
70
|
+
job.snapshot.progress.packages.push(item)
|
|
71
|
+
}
|
|
72
|
+
return item
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function progressFor(job, event) {
|
|
76
|
+
if (!ACTIVE.has(job.snapshot.status) || !event || typeof event !== 'object') return
|
|
77
|
+
// Progress is advisory: malformed events must not break the actual worker.
|
|
78
|
+
try {
|
|
79
|
+
if (event.type === 'stage') {
|
|
80
|
+
if (typeof event.stage === 'string') job.snapshot.progress.stage = event.stage.slice(0, 120)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
if (event.type !== 'package-start' && event.type !== 'package-complete') return
|
|
84
|
+
const item = packageFor(job, event.package)
|
|
85
|
+
if (!item || PACKAGE_DONE.has(item.status)) return
|
|
86
|
+
item.status = event.type === 'package-start' ? 'running' : completionStatus(event.package)
|
|
87
|
+
refreshProgress(job)
|
|
88
|
+
} catch { /* progress cannot affect the final workbench response */ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function finish(job, status, result = null) {
|
|
92
|
+
if (!ACTIVE.has(job.snapshot.status)) return
|
|
93
|
+
const state = job.snapshot
|
|
94
|
+
state.status = status
|
|
95
|
+
state.finishedAt = timestamp()
|
|
96
|
+
state.result = result
|
|
97
|
+
state.progress.stage = status
|
|
98
|
+
for (const item of state.progress.packages) {
|
|
99
|
+
if (PACKAGE_DONE.has(item.status)) continue
|
|
100
|
+
// Pending packages were never observed running; once the worker settles,
|
|
101
|
+
// they cannot start. An interrupted running package has a known outcome.
|
|
102
|
+
item.status = status === 'cancelled' ? 'cancelled'
|
|
103
|
+
: item.status === 'running' ? (status === 'failed' ? 'failed' : 'succeeded') : 'skipped'
|
|
104
|
+
}
|
|
105
|
+
refreshProgress(job)
|
|
106
|
+
if (status === 'failed') state.error = FAILURE
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function fail(job) {
|
|
110
|
+
finish(job, job.controller.signal.aborted ? 'cancelled' : 'failed')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function requestCancel(job) {
|
|
114
|
+
if (ACTIVE.has(job.snapshot.status) && !job.snapshot.cancelRequested) {
|
|
115
|
+
job.snapshot.cancelRequested = true
|
|
116
|
+
job.snapshot.status = 'cancelling'
|
|
117
|
+
job.snapshot.progress.stage = 'cancelling'
|
|
118
|
+
job.controller.abort()
|
|
119
|
+
}
|
|
120
|
+
return snapshot(job)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
start({ run, packages = [] } = {}) {
|
|
125
|
+
if (disposed) throw new Error('Run job manager is disposed.')
|
|
126
|
+
if (typeof run !== 'function') throw new TypeError('Run job requires a run function.')
|
|
127
|
+
if ([...jobs.values()].filter(job => ACTIVE.has(job.snapshot.status)).length >= maxActive) {
|
|
128
|
+
throw new Error('Too many active run jobs.')
|
|
129
|
+
}
|
|
130
|
+
// Evict the oldest terminal snapshots first. A cancelling worker still
|
|
131
|
+
// occupies capacity because its process/cleanup may still be running.
|
|
132
|
+
if (jobs.size >= maxJobs) {
|
|
133
|
+
const terminal = [...jobs].find(([, job]) => !ACTIVE.has(job.snapshot.status))
|
|
134
|
+
if (!terminal) throw new Error('Run job capacity reached; active jobs are still running.')
|
|
135
|
+
jobs.delete(terminal[0])
|
|
136
|
+
}
|
|
137
|
+
const job = {
|
|
138
|
+
controller: new AbortController(),
|
|
139
|
+
snapshot: {
|
|
140
|
+
jobId: randomUUID(), status: 'running', startedAt: timestamp(), finishedAt: null,
|
|
141
|
+
cancelRequested: false,
|
|
142
|
+
progress: { total: 0, completed: 0, current: null, stage: 'starting', packages: [] },
|
|
143
|
+
result: null,
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
for (const item of Array.isArray(packages) ? packages : []) packageFor(job, item)
|
|
147
|
+
refreshProgress(job)
|
|
148
|
+
jobs.set(job.snapshot.jobId, job)
|
|
149
|
+
// Both rejection paths are attached before the worker can run. Result
|
|
150
|
+
// cloning also detaches the worker's mutable response from stored state.
|
|
151
|
+
Promise.resolve().then(() => run({
|
|
152
|
+
signal: job.controller.signal,
|
|
153
|
+
onProgress: event => progressFor(job, event),
|
|
154
|
+
})).then(result => {
|
|
155
|
+
const value = result == null ? null : copy(result)
|
|
156
|
+
const cancelled = value?.status === 'cancelled' || value?.execution?.status === 'cancelled'
|
|
157
|
+
finish(job, cancelled ? 'cancelled' : 'completed', value)
|
|
158
|
+
}, () => fail(job)).catch(() => fail(job))
|
|
159
|
+
return snapshot(job)
|
|
160
|
+
},
|
|
161
|
+
status(jobId) { return snapshot(find(jobId)) },
|
|
162
|
+
cancel(jobId) { return requestCancel(find(jobId)) },
|
|
163
|
+
dispose() {
|
|
164
|
+
if (disposed) return
|
|
165
|
+
disposed = true
|
|
166
|
+
for (const job of jobs.values()) requestCancel(job)
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -1223,6 +1223,12 @@ function aggregateOf(results) {
|
|
|
1223
1223
|
return { status, packages: results, aggregate }
|
|
1224
1224
|
}
|
|
1225
1225
|
|
|
1226
|
+
/** Host-only observation hooks must never discard execution or billing evidence. */
|
|
1227
|
+
export async function observeExecutionProgress(hook, value) {
|
|
1228
|
+
if (typeof hook !== 'function') return
|
|
1229
|
+
try { await hook(value) } catch { /* Progress reporting does not control execution. */ }
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1226
1232
|
function checkedPackageSelections(packages, routes, overrides, runVerified, targets) {
|
|
1227
1233
|
const preceding = new Set()
|
|
1228
1234
|
const selected = {}
|
|
@@ -1299,10 +1305,12 @@ async function runManualTool(target, prompt, options) {
|
|
|
1299
1305
|
*/
|
|
1300
1306
|
async function runPackages({ packages, task, routingBypassed, routes, previous = [], targets = null, overrides = {}, choices = {}, options }) {
|
|
1301
1307
|
const routeList = Array.isArray(routes) ? routes : []
|
|
1302
|
-
const { credentialsFor, onPackage, billing, ...runOptions } = options
|
|
1308
|
+
const { credentialsFor, onPackageStart, onPackage, onStage, billing, ...runOptions } = options
|
|
1309
|
+
await observeExecutionProgress(onStage, 'preparing')
|
|
1303
1310
|
const effectivePackages = checkedPackageSelections(packages, routeList, overrides, runOptions.runVerified, targets)
|
|
1304
1311
|
const results = []
|
|
1305
1312
|
const ranIds = []
|
|
1313
|
+
if (!runOptions.signal?.aborted) await observeExecutionProgress(onStage, 'executing')
|
|
1306
1314
|
for (const item of effectivePackages) {
|
|
1307
1315
|
const kept = previous.find(result => result.id === item.id)
|
|
1308
1316
|
if (targets && !targets.has(item.id) && kept) {
|
|
@@ -1315,14 +1323,17 @@ async function runPackages({ packages, task, routingBypassed, routes, previous =
|
|
|
1315
1323
|
const provider = target?.kind === 'tool' ? 'official-tool' : target?.provider ?? override?.provider ?? item.recommendedProvider
|
|
1316
1324
|
const model = target?.kind === 'tool' ? target.toolId : target?.model ?? override?.model ?? item.recommendedModel
|
|
1317
1325
|
const identity = { ...selection, ...(item.type ? { type: item.type } : {}), ...(item.purpose ? { purpose: item.purpose } : {}) }
|
|
1318
|
-
|
|
1326
|
+
const cancelledBeforeStart = async () => {
|
|
1319
1327
|
// Cancelled: later packages are not started (no CLI process, no API spend).
|
|
1320
1328
|
const cancelled = { id: item.id, name: item.name, ok: false, provider, model,
|
|
1321
1329
|
channel: target?.kind === 'tool' ? 'official-cli' : 'harness-llm', ...(target?.kind === 'tool' ? { toolId: target.toolId } : {}),
|
|
1322
1330
|
...identity, answer: '', fallback: null, cancelled: true, notStarted: true, error: '执行已取消,此步骤未开始。' }
|
|
1323
1331
|
results.push(cancelled)
|
|
1324
1332
|
ranIds.push(item.id)
|
|
1325
|
-
|
|
1333
|
+
await observeExecutionProgress(onPackage, cancelled)
|
|
1334
|
+
}
|
|
1335
|
+
if (runOptions.signal?.aborted) {
|
|
1336
|
+
await cancelledBeforeStart()
|
|
1326
1337
|
continue
|
|
1327
1338
|
}
|
|
1328
1339
|
const unmet = (item.dependsOn ?? []).filter(id => results.find(result => result.id === id)?.ok !== true)
|
|
@@ -1338,7 +1349,7 @@ async function runPackages({ packages, task, routingBypassed, routes, previous =
|
|
|
1338
1349
|
}
|
|
1339
1350
|
results.push(blocked)
|
|
1340
1351
|
ranIds.push(item.id)
|
|
1341
|
-
|
|
1352
|
+
await observeExecutionProgress(onPackage, blocked)
|
|
1342
1353
|
continue
|
|
1343
1354
|
}
|
|
1344
1355
|
const plannedRoute = routeList.find(candidate => candidate.provider === provider && candidate.model === model)
|
|
@@ -1347,13 +1358,24 @@ async function runPackages({ packages, task, routingBypassed, routes, previous =
|
|
|
1347
1358
|
: target?.kind === 'tool' ? { provider, model } : reasoningRouteForAssignment(plannedRoute, item, override)
|
|
1348
1359
|
if (target?.kind === 'api') delete route.reasoningEffort
|
|
1349
1360
|
const promptText = routingBypassed ? checkedTask(task) : packagePrompt(task, item, results)
|
|
1361
|
+
const credentials = !target ? (typeof credentialsFor === 'function' ? await credentialsFor(route) : runOptions.credentials ?? null) : null
|
|
1362
|
+
if (runOptions.signal?.aborted) {
|
|
1363
|
+
await cancelledBeforeStart()
|
|
1364
|
+
continue
|
|
1365
|
+
}
|
|
1350
1366
|
const startedAt = Date.now()
|
|
1367
|
+
await observeExecutionProgress(onPackageStart, { ...item, ...identity, provider, model, startedAt,
|
|
1368
|
+
channel: target?.kind === 'tool' ? 'official-cli' : 'harness-llm',
|
|
1369
|
+
...(target?.kind === 'tool' ? { toolId: target.toolId } : {}) })
|
|
1370
|
+
if (runOptions.signal?.aborted) {
|
|
1371
|
+
await cancelledBeforeStart()
|
|
1372
|
+
continue
|
|
1373
|
+
}
|
|
1351
1374
|
let result
|
|
1352
1375
|
if (target?.kind === 'tool') result = await runManualTool(target, promptText, runOptions)
|
|
1353
1376
|
else if (target?.kind === 'api') result = { ...await useApi(runOptions.apiFallback, { route, task: promptText,
|
|
1354
1377
|
signal: runOptions.signal, preference: 'api', reason: '按工作包手动选择模型目录 API。' }), fallback: null }
|
|
1355
1378
|
else {
|
|
1356
|
-
const credentials = typeof credentialsFor === 'function' ? await credentialsFor(route) : runOptions.credentials ?? null
|
|
1357
1379
|
result = await executeRouteWithBilling({
|
|
1358
1380
|
...runOptions,
|
|
1359
1381
|
billing: billing ? { ...billing, routes: billing.routes ?? routeList, ...(choices[item.id] ? { choice: choices[item.id] } : {}) } : null,
|
|
@@ -1382,7 +1404,7 @@ async function runPackages({ packages, task, routingBypassed, routes, previous =
|
|
|
1382
1404
|
}
|
|
1383
1405
|
results.push(entry)
|
|
1384
1406
|
ranIds.push(item.id)
|
|
1385
|
-
|
|
1407
|
+
await observeExecutionProgress(onPackage, entry)
|
|
1386
1408
|
}
|
|
1387
1409
|
return { ...aggregateOf(results), ranIds }
|
|
1388
1410
|
}
|