@hecer/yoke 1.20.0 → 1.22.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +46 -0
- package/README.md +6 -1
- package/bench/analyze-codex-comparison.mjs +90 -17
- package/bench/compare-codex.mjs +159 -36
- package/bench/result-schema.mjs +132 -0
- package/canon/manifest.yaml +1 -1
- package/dist/agents/pi-telemetry.js +2 -1
- package/dist/agents/process-streams.js +12 -64
- package/dist/agents/provider-selection.js +12 -0
- package/dist/agents/telemetry.js +52 -52
- package/dist/change/inbox.js +8 -3
- package/dist/check/command.js +69 -17
- package/dist/check/delivery.js +121 -0
- package/dist/cli.js +86 -8
- package/dist/code-intelligence/budgets.js +138 -0
- package/dist/code-intelligence/contracts.js +2 -0
- package/dist/code-intelligence/coordinator.js +156 -84
- package/dist/code-intelligence/evidence.js +87 -34
- package/dist/code-intelligence/mcp-client.js +10 -2
- package/dist/code-intelligence/mcp-server.js +10 -10
- package/dist/dashboard/analytics.js +5 -3
- package/dist/goals/command.js +183 -53
- package/dist/goals/usage.js +87 -0
- package/dist/loop/candidate-cleanup.js +47 -17
- package/dist/loop/candidates.js +17 -11
- package/dist/loop/cleanup.js +100 -0
- package/dist/loop/dispatcher.js +89 -26
- package/dist/loop/failure.js +104 -0
- package/dist/loop/gate-snapshot.js +19 -0
- package/dist/loop/git.js +1 -1
- package/dist/loop/loop.js +100 -66
- package/dist/loop/parallel-adapters.js +22 -4
- package/dist/loop/parallel-command.js +32 -5
- package/dist/loop/recovery.js +23 -5
- package/dist/loop/reporter.js +21 -4
- package/dist/loop/run-command.js +102 -48
- package/dist/loop/runner.js +5 -4
- package/dist/loop/worker.js +145 -91
- package/dist/observability/history.js +1 -1
- package/dist/observability/invocation.js +42 -0
- package/dist/observability/usage.js +17 -0
- package/dist/prd/command.js +20 -7
- package/dist/prd/decompose.js +5 -2
- package/dist/retrofit/command.js +22 -0
- package/dist/retrofit/config.js +26 -1
- package/dist/retrofit/gitignore.js +2 -0
- package/dist/retrofit/planners/claude.js +4 -4
- package/dist/retrofit/wsl.js +23 -3
- package/dist/routing/attempts.js +241 -0
- package/dist/routing/capability.js +13 -9
- package/dist/routing/optimization.js +73 -0
- package/dist/routing/registry.js +7 -1
- package/dist/routing/router.js +280 -127
- package/dist/setup/command.js +54 -6
- package/dist/smoke/command.js +302 -74
- package/docs/BENCHMARK-MANIFEST.md +131 -0
- package/docs/CODE-INTELLIGENCE.md +43 -1
- package/docs/CODEX-COMPARISON-2026-09-29.md +15 -0
- package/docs/DELIVERY-JOURNEYS.md +199 -0
- package/docs/ECONOMIC-ROUTING.md +180 -0
- package/docs/GOALS.md +61 -4
- package/docs/RELEASE-VALIDATION-1.22.0.md +115 -0
- package/docs/parallel-execution.md +37 -9
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
package/dist/routing/router.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { buildWatchdogInvocation, makeRunner, runCapturedAgent, runnerInvocation, contextBlockFor, } from '../loop/runner.js';
|
|
2
3
|
import { isAcceptanceCriterion, criterionCommandProblem } from '../loop/prd.js';
|
|
3
4
|
import { historyForWorkers, projectHash, readRoutingObservations, recordRoutingObservation, storyHash } from './registry.js';
|
|
4
5
|
import { assessmentInstructions, parseAssessment, tiers } from './assessment.js';
|
|
5
6
|
import { chooseCapability, readAssessment, saveAssessment, routingAssessmentKey, knownInfrastructureFailure } from './capability.js';
|
|
6
7
|
import { readPlanningFile } from './contracts.js';
|
|
8
|
+
import { finishRoutingAttempt, markRoutingAttemptUsageIncomplete, readRoutingAttempts, recordRoutingAttemptUsage, reserveRoutingAttempt, routingEpisodeSummary } from './attempts.js';
|
|
9
|
+
import { assessmentSignature } from './optimization.js';
|
|
10
|
+
export function buildAssessmentPrompt(root, ctx) {
|
|
11
|
+
return [assessmentInstructions, 'Use the supplied task contract and project context to produce a bounded plan. Do not implement or change files.',
|
|
12
|
+
'Approved planning brief:', readPlanningFile(root, '.yoke/plan.md', 80_000) ?? '',
|
|
13
|
+
contextBlockFor(ctx.targetDir, ctx.story), JSON.stringify(ctx.story), 'Return exactly one line: YOKE_ASSESS {"taskClass":"implementation","difficulty":"medium","uncertainty":"low","risk":"low","scope":"low","testability":"high","reason":"evidence","approach":"steps and tests"}'].join('\n');
|
|
14
|
+
}
|
|
7
15
|
const costRank = { low: 0, medium: 1, high: 2 };
|
|
8
16
|
export function rankWorkers(workers, strategy, maxCandidates) {
|
|
9
17
|
const history = historyForWorkers(workers);
|
|
@@ -113,6 +121,7 @@ function callUsage(role, provider, selection, tokens, durationMs, profile) {
|
|
|
113
121
|
usageAvailable: tokens !== undefined && tokens.measurementComplete !== false,
|
|
114
122
|
...(tokens?.reasoningOutputTokens !== undefined ? { reasoningOutputTokens: tokens.reasoningOutputTokens } : {}),
|
|
115
123
|
...(tokens?.totalCostUsd !== undefined ? { totalCostUsd: tokens.totalCostUsd } : {}),
|
|
124
|
+
costMeasurementComplete: tokens?.totalCostUsd !== undefined && tokens.costMeasurementComplete !== false,
|
|
116
125
|
durationMs,
|
|
117
126
|
};
|
|
118
127
|
}
|
|
@@ -121,8 +130,17 @@ export function makeAdaptiveRunner(options) {
|
|
|
121
130
|
return ctx => {
|
|
122
131
|
const steps = run(ctx);
|
|
123
132
|
let next = steps.next();
|
|
124
|
-
while (!next.done)
|
|
125
|
-
|
|
133
|
+
while (!next.done) {
|
|
134
|
+
let result;
|
|
135
|
+
try {
|
|
136
|
+
result = next.value();
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
next = steps.throw(error);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
next = steps.next(result);
|
|
143
|
+
}
|
|
126
144
|
return next.value;
|
|
127
145
|
};
|
|
128
146
|
}
|
|
@@ -131,8 +149,17 @@ export function makeAsyncAdaptiveRunner(options) {
|
|
|
131
149
|
return async (ctx) => {
|
|
132
150
|
const steps = run(ctx);
|
|
133
151
|
let next = steps.next();
|
|
134
|
-
while (!next.done)
|
|
135
|
-
|
|
152
|
+
while (!next.done) {
|
|
153
|
+
let result;
|
|
154
|
+
try {
|
|
155
|
+
result = await next.value();
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
next = steps.throw(error);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
next = steps.next(result);
|
|
162
|
+
}
|
|
136
163
|
return next.value;
|
|
137
164
|
};
|
|
138
165
|
}
|
|
@@ -142,26 +169,100 @@ function routingSteps(options) {
|
|
|
142
169
|
const eligibleWorkers = options.workers.filter(worker => available(worker.agent));
|
|
143
170
|
const failedStories = new Set();
|
|
144
171
|
const makeWorker = options.makeWorker ?? ((agent, selection) => makeRunner(agent, options.idleTimeoutMs ?? 0, {
|
|
145
|
-
...options.runnerOpts,
|
|
146
|
-
permissions: options.permissions ?? 'safe',
|
|
147
|
-
selection,
|
|
172
|
+
...options.runnerOpts, permissions: options.permissions ?? 'safe', selection,
|
|
148
173
|
}));
|
|
149
174
|
return function* (ctx) {
|
|
150
|
-
const
|
|
175
|
+
const root = options.projectRoot ?? ctx.targetDir;
|
|
176
|
+
const calls = [];
|
|
177
|
+
let reservation;
|
|
178
|
+
const routeStartedAt = new Date().toISOString();
|
|
179
|
+
const blocked = (summary) => ({ success: false, summary,
|
|
180
|
+
...(calls.length ? { tokens: { ...aggregateCalls(calls), storyId: ctx.story.id, ...(reservation ? { routingAttemptId: reservation.id } : {}) } } : {}),
|
|
181
|
+
routing: { blocked: true, recordOutcome: () => undefined },
|
|
182
|
+
});
|
|
183
|
+
function* perform(role, provider, selection, work, profile, beforeInvoke) {
|
|
184
|
+
const request = { callId: randomUUID(), storyId: ctx.story.id, role, provider, selection: { ...selection } };
|
|
185
|
+
try {
|
|
186
|
+
if (!available(provider))
|
|
187
|
+
return { blocked: `Configured ${role} provider is unavailable` };
|
|
188
|
+
const refusal = options.admitCall?.(request);
|
|
189
|
+
if (refusal)
|
|
190
|
+
return { blocked: refusal };
|
|
191
|
+
beforeInvoke?.();
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
return { blocked: `Call admission failed: ${error.message}` };
|
|
195
|
+
}
|
|
196
|
+
const started = now();
|
|
197
|
+
let result;
|
|
198
|
+
try {
|
|
199
|
+
result = yield work;
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
const reported = error && typeof error === 'object' ? error.tokens : undefined;
|
|
203
|
+
result = { success: false, infrastructureFailure: true, summary: error?.message ?? String(error), output: '', ...(reported ? { tokens: reported } : {}) };
|
|
204
|
+
}
|
|
205
|
+
const durationMs = Math.max(0, now() - started);
|
|
206
|
+
const callId = result.tokens?.callId ?? (result.tokens?.calls?.length === 1 ? result.tokens.calls[0].callId : undefined) ?? request.callId;
|
|
207
|
+
const call = { ...callUsage(role, provider, selection, result.tokens, durationMs, profile), callId,
|
|
208
|
+
...(reservation ? { routingAttemptId: reservation.id } : {}) };
|
|
209
|
+
calls.push(call);
|
|
210
|
+
const usage = { ...aggregateCalls([call]), callId, storyId: ctx.story.id,
|
|
211
|
+
...(reservation ? { routingAttemptId: reservation.id } : {}) };
|
|
212
|
+
let accountingError;
|
|
213
|
+
try {
|
|
214
|
+
// Earlier planning calls acquire the same execution identity once a worker
|
|
215
|
+
// has actually been admitted. Stable call IDs make reporter joins idempotent.
|
|
216
|
+
if (reservation)
|
|
217
|
+
recordRoutingAttemptUsage(root, reservation.id, { ...aggregateCalls(calls), routingAttemptId: reservation.id });
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
accountingError = error;
|
|
221
|
+
}
|
|
222
|
+
// A failed optional join must never suppress delivery of already paid
|
|
223
|
+
// usage to the caller's own durable budget account.
|
|
224
|
+
try {
|
|
225
|
+
options.onCallUsage?.({ ...request, callId, durationMs, usage });
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
accountingError ??= error;
|
|
229
|
+
}
|
|
230
|
+
if (accountingError) {
|
|
231
|
+
if (reservation) {
|
|
232
|
+
try {
|
|
233
|
+
markRoutingAttemptUsageIncomplete(root, reservation.id);
|
|
234
|
+
}
|
|
235
|
+
catch { /* unreadable state cannot provide a complete sample */ }
|
|
236
|
+
try {
|
|
237
|
+
finishRoutingAttempt(root, reservation, { verificationSuccess: false, failureKind: 'infrastructure', actualModel: result.tokens?.model });
|
|
238
|
+
}
|
|
239
|
+
catch { /* reservation remains charged and unresolved */ }
|
|
240
|
+
}
|
|
241
|
+
return { blocked: `Call accounting failed: ${accountingError.message}` };
|
|
242
|
+
}
|
|
243
|
+
return { result };
|
|
244
|
+
}
|
|
245
|
+
const capture = (provider, prompt, selection) => () => options.captureRoute
|
|
246
|
+
? options.captureRoute(provider, ctx, prompt, selection)
|
|
247
|
+
: runCapturedAgent(provider, buildWatchdogInvocation(runnerInvocation(provider, prompt, ctx.targetDir, true, 'read-only', selection), options.idleTimeoutMs ?? 0));
|
|
248
|
+
const reserveWorker = (provider, selection, profile, limit) => {
|
|
249
|
+
reservation = reserveRoutingAttempt({ root, storyId: ctx.story.id, contractKey: routingAssessmentKey(root, ctx.story), limit,
|
|
250
|
+
provider, profile, selection, startedAt: routeStartedAt, accountingScope: options.accountingScope,
|
|
251
|
+
executionPolicyKey: options.executionPolicyKey });
|
|
252
|
+
};
|
|
151
253
|
if (options.strategy === 'capability' && options.assessmentPolicy === 'prepared') {
|
|
152
254
|
try {
|
|
153
|
-
const criteria = ctx.story.acceptance;
|
|
154
|
-
if (criteria.length < 2 || criteria.length > 5 || criteria.some(c => !isAcceptanceCriterion(c) || criterionCommandProblem(c)))
|
|
155
|
-
return blocked('Prepared routing requires 2-5 executable acceptance criteria');
|
|
156
|
-
if (!readAssessment(
|
|
157
|
-
return blocked(
|
|
255
|
+
const criteria = ctx.story.acceptance, goal = options.contractKind === 'goal';
|
|
256
|
+
if (criteria.length < (goal ? 1 : 2) || (!goal && criteria.length > 5) || criteria.some(c => !isAcceptanceCriterion(c) || (!goal && criterionCommandProblem(c)) || (goal && (!c.verify.length || c.verify.some(command => !command.trim())))))
|
|
257
|
+
return blocked(goal ? 'Prepared goal routing requires bound executable acceptance criteria' : 'Prepared routing requires 2-5 executable acceptance criteria');
|
|
258
|
+
if (!readAssessment(root, ctx.story, true))
|
|
259
|
+
return blocked(`Task assessment is missing or stale. Run yoke ${goal ? 'goal' : 'prd'} assess before execution.`);
|
|
158
260
|
}
|
|
159
261
|
catch (error) {
|
|
160
262
|
return blocked(`Cannot read prepared assessment: ${error.message}`);
|
|
161
263
|
}
|
|
162
264
|
}
|
|
163
265
|
if (options.strategy === 'capability' && !options.rules?.some(rule => (!rule.area || rule.area === ctx.story.area) && (!rule.storyId || rule.storyId === ctx.story.id))) {
|
|
164
|
-
const root = options.projectRoot ?? ctx.targetDir;
|
|
165
266
|
let assessment;
|
|
166
267
|
try {
|
|
167
268
|
assessment = readAssessment(root, ctx.story, options.assessmentPolicy === 'prepared');
|
|
@@ -169,79 +270,151 @@ function routingSteps(options) {
|
|
|
169
270
|
catch (error) {
|
|
170
271
|
return blocked(`Cannot read assessment: ${error.message}`);
|
|
171
272
|
}
|
|
172
|
-
let planning;
|
|
173
|
-
const calls = [];
|
|
174
273
|
if (!assessment) {
|
|
175
|
-
const inputKey = routingAssessmentKey(root, ctx.story);
|
|
176
|
-
const prompt = [assessmentInstructions, 'Use the supplied task contract and project context to produce a bounded plan. Do not implement or change files.',
|
|
177
|
-
'Approved planning brief:', readPlanningFile(root, '.yoke/plan.md', 80_000) ?? '',
|
|
178
|
-
contextBlockFor(ctx.targetDir, ctx.story), JSON.stringify(ctx.story), 'Return exactly one line: YOKE_ASSESS {"taskClass":"implementation","difficulty":"medium","uncertainty":"low","risk":"low","scope":"low","testability":"high","reason":"evidence","approach":"steps and tests"}'].join('\n');
|
|
179
274
|
const planner = options.planner?.agent ?? options.parent;
|
|
180
|
-
if (!available(planner))
|
|
181
|
-
return blocked('Configured planning provider is unavailable');
|
|
182
275
|
const selection = { ...(options.planner?.selection ?? options.parentSelection), nativeMultiAgent: false };
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
276
|
+
let inputKey, prompt;
|
|
277
|
+
try {
|
|
278
|
+
inputKey = routingAssessmentKey(root, ctx.story);
|
|
279
|
+
prompt = buildAssessmentPrompt(root, ctx);
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
return blocked(`Cannot prepare assessment: ${error.message}`);
|
|
283
|
+
}
|
|
284
|
+
const planningCall = yield* perform('orchestrator', planner, selection, capture(planner, prompt, selection));
|
|
285
|
+
if (planningCall.blocked)
|
|
286
|
+
return blocked(planningCall.blocked);
|
|
287
|
+
const planning = planningCall.result;
|
|
288
|
+
try {
|
|
289
|
+
if (routingAssessmentKey(root, ctx.story) !== inputKey)
|
|
290
|
+
return blocked('Planning inputs changed during assessment; retry planning with the current contract');
|
|
291
|
+
assessment = planning.success ? parseAssessment(planning.output) : undefined;
|
|
292
|
+
if (assessment)
|
|
293
|
+
saveAssessment(root, ctx.story, assessment, { provider: planner, model: planning.tokens?.model ?? selection.model });
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
return blocked(`Cannot persist current assessment: ${error.message}`);
|
|
297
|
+
}
|
|
192
298
|
}
|
|
193
299
|
if (!assessment)
|
|
194
|
-
return
|
|
195
|
-
|
|
300
|
+
return blocked('Routing assessment unavailable or invalid; implementation was not started');
|
|
301
|
+
let choice;
|
|
302
|
+
try {
|
|
303
|
+
choice = chooseCapability({ root, story: ctx.story, assessment, workers: eligibleWorkers, parent: options.parent, parentSelection: options.parentSelection,
|
|
304
|
+
maxAttempts: options.maxAttempts, fallback: options.fallback, maxTier: options.maxTier,
|
|
305
|
+
optimization: options.optimization, executionPolicyKey: options.executionPolicyKey });
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
return blocked(`Cannot admit routing attempt: ${error.message}`);
|
|
309
|
+
}
|
|
196
310
|
options.onDecision?.(ctx.story.id, { profile: choice.worker?.id ?? 'SELF', provider: choice.provider, model: choice.selection.model, reasoningEffort: choice.selection.reasoningEffort, variant: choice.selection.variant, providerModel: choice.selection.provider, reason: choice.reason, next: choice.next, assessment });
|
|
197
311
|
if (choice.blocked)
|
|
198
|
-
return
|
|
312
|
+
return blocked(choice.reason);
|
|
199
313
|
if (choice.exhausted)
|
|
200
|
-
return
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
314
|
+
return blocked('Routing attempt budget exhausted; replan this task before retrying');
|
|
315
|
+
let runner;
|
|
316
|
+
const implementation = yield* perform(choice.worker ? 'worker' : 'parent', choice.provider, choice.selection, () => runner({ ...ctx, attempt: reservation.ordinal, story: { ...ctx.story, assessment } }), choice.worker?.id ?? 'SELF', () => { runner = makeWorker(choice.provider, choice.selection); reserveWorker(choice.provider, choice.selection, choice.worker?.id ?? 'SELF', choice.attemptLimit); });
|
|
317
|
+
if (implementation.blocked)
|
|
318
|
+
return blocked(implementation.blocked);
|
|
319
|
+
const result = implementation.result;
|
|
204
320
|
let recorded = false;
|
|
205
321
|
const infrastructureFailure = result.infrastructureFailure || (!result.success && knownInfrastructureFailure(result.summary));
|
|
322
|
+
const recordOutcome = (verified, failureKind) => {
|
|
323
|
+
if (recorded)
|
|
324
|
+
return;
|
|
325
|
+
const kind = infrastructureFailure ? 'infrastructure' : failureKind ?? 'implementation';
|
|
326
|
+
const measured = finishRoutingAttempt(root, reservation, { verificationSuccess: infrastructureFailure ? false : verified, failureKind: kind, actualModel: result.tokens?.model });
|
|
327
|
+
const episode = routingEpisodeSummary(root, reservation);
|
|
328
|
+
recorded = true;
|
|
329
|
+
recordRoutingObservation({ projectHash: projectHash(root), storyHash: storyHash(projectHash(root), ctx.story.id), assessmentKey: reservation.contractKey, taskClass: assessment.taskClass, requiredTier: choice.requiredTier,
|
|
330
|
+
role: 'implementation', strategy: 'capability', selected: choice.worker?.id ?? 'SELF', provider: choice.provider,
|
|
331
|
+
requestedProvider: choice.selection.provider, requestedModel: choice.selection.model, requestedReasoningEffort: choice.selection.reasoningEffort, requestedVariant: choice.selection.variant,
|
|
332
|
+
actualModel: result.tokens?.model, orchestratorProvider: options.planner?.agent ?? options.parent, orchestratorModel: (options.planner?.selection ?? options.parentSelection)?.model,
|
|
333
|
+
orchestratorDurationMs: calls.filter(c => c.role === 'orchestrator').reduce((sum, call) => sum + call.durationMs, 0), workerDurationMs: calls.at(-1).durationMs,
|
|
334
|
+
processSuccess: result.success, verificationSuccess: infrastructureFailure ? false : verified, failureKind: kind,
|
|
335
|
+
usageAvailable: measured.usageComplete, inputTokens: measured.inputTokens, outputTokens: measured.outputTokens,
|
|
336
|
+
totalCostUsd: measured.totalCostUsd, costMeasurementComplete: measured.costComplete, accountingScope: measured.accountingScope,
|
|
337
|
+
totalDurationMs: measured.durationMs, executionPolicyKey: options.executionPolicyKey,
|
|
338
|
+
economicEpisode: { ...episode, assessmentSignature: assessmentSignature(assessment) },
|
|
339
|
+
});
|
|
340
|
+
};
|
|
341
|
+
if (infrastructureFailure)
|
|
342
|
+
recordOutcome(false, 'infrastructure');
|
|
206
343
|
return { ...result, summary: `route=${choice.worker?.id ?? 'SELF'} (${choice.reason}); ${result.summary}`,
|
|
207
344
|
...(infrastructureFailure ? { success: false, infrastructureFailure: true } : {}),
|
|
208
|
-
tokens: { ...aggregateCalls(calls), storyId: ctx.story.id, escalated: choice.failures > 1 },
|
|
209
|
-
routing: { blocked: infrastructureFailure || undefined, canRetry: !infrastructureFailure &&
|
|
210
|
-
if (recorded)
|
|
211
|
-
return;
|
|
212
|
-
recorded = true;
|
|
213
|
-
recordRoutingObservation({ projectHash: projectHash(root), storyHash: storyHash(projectHash(root), ctx.story.id), assessmentKey: routingAssessmentKey(root, ctx.story), taskClass: assessment.taskClass, requiredTier: choice.requiredTier,
|
|
214
|
-
role: 'implementation', strategy: 'capability', selected: choice.worker?.id ?? 'SELF', provider: choice.provider, requestedProvider: choice.selection.provider, requestedModel: choice.selection.model, requestedReasoningEffort: choice.selection.reasoningEffort, requestedVariant: choice.selection.variant,
|
|
215
|
-
actualModel: result.tokens?.model, orchestratorProvider: options.planner?.agent ?? options.parent, orchestratorModel: (options.planner?.selection ?? options.parentSelection)?.model, orchestratorDurationMs: calls.filter(c => c.role === 'orchestrator').reduce((s, c) => s + c.durationMs, 0), workerDurationMs: calls[calls.length - 1].durationMs,
|
|
216
|
-
processSuccess: result.success, verificationSuccess: infrastructureFailure ? false : verified, failureKind: infrastructureFailure ? 'infrastructure' : failureKind ?? 'implementation', usageAvailable: result.tokens !== undefined && result.tokens.measurementComplete !== false,
|
|
217
|
-
inputTokens: result.tokens?.inputTokens ?? 0, outputTokens: result.tokens?.outputTokens ?? 0, totalCostUsd: result.tokens?.totalCostUsd });
|
|
218
|
-
} } };
|
|
345
|
+
tokens: { ...aggregateCalls(calls), storyId: ctx.story.id, routingAttemptId: reservation.id, escalated: choice.failures > 1 },
|
|
346
|
+
routing: { blocked: infrastructureFailure || undefined, canRetry: !infrastructureFailure && reservation.ordinal < choice.attemptLimit, recordOutcome } };
|
|
219
347
|
}
|
|
220
|
-
// Re-rank per story so a long-running loop can use gate outcomes learned by
|
|
221
|
-
// earlier stories without rebuilding the runner.
|
|
222
348
|
const rule = options.rules?.find(rule => (!rule.area || rule.area === ctx.story.area) && (!rule.storyId || rule.storyId === ctx.story.id) && (rule.area || rule.storyId));
|
|
349
|
+
const bounded = options.strategy === 'capability' || options.maxAttempts !== undefined;
|
|
350
|
+
let priorAttempts = [];
|
|
351
|
+
let failureKey = ctx.story.id;
|
|
352
|
+
if (bounded) {
|
|
353
|
+
try {
|
|
354
|
+
failureKey = routingAssessmentKey(root, ctx.story);
|
|
355
|
+
priorAttempts = readRoutingAttempts(root, ctx.story.id, failureKey);
|
|
356
|
+
if (priorAttempts.length >= (options.maxAttempts ?? 5))
|
|
357
|
+
return blocked('Routing attempt budget exhausted; replan this task before retrying');
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
return blocked(`Cannot read routing attempt state: ${error.message}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
223
363
|
if (rule) {
|
|
224
|
-
const project = projectHash(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
364
|
+
const project = projectHash(root);
|
|
365
|
+
try {
|
|
366
|
+
const prior = bounded ? priorAttempts.filter(entry => entry.outcome?.failureKind !== 'infrastructure').at(-1)?.outcome
|
|
367
|
+
: readRoutingObservations().reverse().find(event => event.projectHash === project && event.storyHash === storyHash(project, ctx.story.id) && typeof event.verificationSuccess === 'boolean');
|
|
368
|
+
if (prior?.verificationSuccess === false)
|
|
369
|
+
failedStories.add(failureKey);
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
return blocked(`Cannot read routing attempt state: ${error.message}`);
|
|
373
|
+
}
|
|
228
374
|
}
|
|
229
|
-
const ruleWorker = rule && failedStories.has(
|
|
375
|
+
const ruleWorker = rule && failedStories.has(failureKey) ? rule.escalateTo ?? 'SELF' : rule?.worker;
|
|
230
376
|
const candidates = rule ? eligibleWorkers : rankWorkers(eligibleWorkers, options.strategy, options.maxCandidates);
|
|
231
377
|
if (candidates.length === 0) {
|
|
232
378
|
if (options.fallback === 'block' || options.maxTier)
|
|
233
379
|
return blocked('No eligible routing profiles; parent fallback is disabled');
|
|
234
|
-
|
|
380
|
+
let runner;
|
|
381
|
+
const selection = options.parentSelection ?? {};
|
|
382
|
+
options.onDecision?.(ctx.story.id, { profile: 'SELF', provider: options.parent, model: selection.model, providerModel: selection.provider,
|
|
383
|
+
reasoningEffort: selection.reasoningEffort, variant: selection.variant, reason: 'No eligible profile; configured parent fallback', next: 'independent gate verification' });
|
|
384
|
+
const execution = yield* perform('parent', options.parent, selection, () => runner(ctx), 'SELF', () => {
|
|
385
|
+
runner = makeWorker(options.parent, selection);
|
|
386
|
+
if (bounded)
|
|
387
|
+
reserveWorker(options.parent, selection, 'SELF', options.maxAttempts ?? 5);
|
|
388
|
+
});
|
|
389
|
+
if (execution.blocked)
|
|
390
|
+
return blocked(execution.blocked);
|
|
391
|
+
const result = execution.result;
|
|
392
|
+
const infrastructureFailure = result.infrastructureFailure || (!result.success && knownInfrastructureFailure(result.summary));
|
|
393
|
+
let recorded = false;
|
|
394
|
+
const recordOutcome = (verified, failureKind) => {
|
|
395
|
+
if (recorded)
|
|
396
|
+
return;
|
|
397
|
+
const kind = infrastructureFailure ? 'infrastructure' : failureKind ?? 'implementation';
|
|
398
|
+
if (reservation)
|
|
399
|
+
finishRoutingAttempt(root, reservation, { verificationSuccess: infrastructureFailure ? false : verified, failureKind: kind, actualModel: result.tokens?.model });
|
|
400
|
+
recorded = true;
|
|
401
|
+
};
|
|
402
|
+
if (infrastructureFailure)
|
|
403
|
+
recordOutcome(false, 'infrastructure');
|
|
404
|
+
return { ...result, ...(infrastructureFailure ? { success: false, infrastructureFailure: true } : {}),
|
|
405
|
+
tokens: { ...aggregateCalls(calls), storyId: ctx.story.id, ...(reservation ? { routingAttemptId: reservation.id } : {}) },
|
|
406
|
+
routing: { recordOutcome, ...(infrastructureFailure ? { blocked: true } : {}), ...(bounded ? { canRetry: !infrastructureFailure && reservation.ordinal < (options.maxAttempts ?? 5) } : {}) } };
|
|
235
407
|
}
|
|
236
|
-
const prompt = buildRoutingPrompt(ctx, candidates, options.strategy);
|
|
237
408
|
const orchestratorSelection = { ...(options.parentSelection ?? {}), ...(options.orchestratorSelection ?? {}), nativeMultiAgent: false };
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
409
|
+
let routeRun = { success: true, summary: 'Explicit rule', output: '' };
|
|
410
|
+
if (!rule) {
|
|
411
|
+
const routed = yield* perform('orchestrator', options.parent, orchestratorSelection, capture(options.parent, buildRoutingPrompt(ctx, candidates, options.strategy), orchestratorSelection));
|
|
412
|
+
if (routed.blocked)
|
|
413
|
+
return blocked(routed.blocked);
|
|
414
|
+
routeRun = routed.result;
|
|
415
|
+
}
|
|
243
416
|
const decision = rule
|
|
244
|
-
? { worker: ruleWorker === 'SELF' || candidates.some(w => w.id === ruleWorker) ? ruleWorker : 'SELF', reason: failedStories.has(
|
|
417
|
+
? { worker: ruleWorker === 'SELF' || candidates.some(w => w.id === ruleWorker) ? ruleWorker : 'SELF', reason: failedStories.has(failureKey) ? 'gate failure escalated by project rule' : 'explicit project routing rule' }
|
|
245
418
|
: routeRun.success ? parseRouteDecision(routeRun.output, candidates.map(worker => worker.id)) : null;
|
|
246
419
|
const selected = decision?.worker ?? 'SELF';
|
|
247
420
|
const worker = selected === 'SELF' ? undefined : candidates.find(candidate => candidate.id === selected);
|
|
@@ -251,75 +424,55 @@ function routingSteps(options) {
|
|
|
251
424
|
const selection = worker
|
|
252
425
|
? { provider: worker.provider, model: worker.model, reasoningEffort: worker.reasoningEffort, variant: worker.variant, nativeMultiAgent: false, ...(provider !== 'gemini' && provider !== 'qwen' && provider !== 'pi' && provider !== 'hermes' ? { bare: options.parentSelection?.bare } : {}) }
|
|
253
426
|
: { ...(options.parentSelection ?? {}), nativeMultiAgent: false };
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
: {}),
|
|
268
|
-
...((routeRun.tokens?.cacheWriteInputTokens !== undefined || result.tokens?.cacheWriteInputTokens !== undefined)
|
|
269
|
-
? { cacheWriteInputTokens: (routeRun.tokens?.cacheWriteInputTokens ?? 0) + (result.tokens?.cacheWriteInputTokens ?? 0) }
|
|
270
|
-
: {}),
|
|
271
|
-
outputTokens: (routeRun.tokens?.outputTokens ?? 0) + (result.tokens?.outputTokens ?? 0),
|
|
272
|
-
...((routeRun.tokens?.reasoningOutputTokens !== undefined || result.tokens?.reasoningOutputTokens !== undefined)
|
|
273
|
-
? { reasoningOutputTokens: (routeRun.tokens?.reasoningOutputTokens ?? 0) + (result.tokens?.reasoningOutputTokens ?? 0) }
|
|
274
|
-
: {}),
|
|
275
|
-
...((routeRun.tokens?.totalCostUsd !== undefined || result.tokens?.totalCostUsd !== undefined)
|
|
276
|
-
? { totalCostUsd: (routeRun.tokens?.totalCostUsd ?? 0) + (result.tokens?.totalCostUsd ?? 0) }
|
|
277
|
-
: {}),
|
|
278
|
-
...(result.tokens?.model ? { model: result.tokens.model } : routeRun.tokens?.model ? { model: routeRun.tokens.model } : {}),
|
|
279
|
-
calls,
|
|
280
|
-
measurementComplete: calls.every(call => call.usageAvailable),
|
|
281
|
-
costMeasurementComplete: calls.every(call => call.totalCostUsd !== undefined),
|
|
282
|
-
};
|
|
427
|
+
options.onDecision?.(ctx.story.id, { profile: selected, provider, model: selection.model, providerModel: selection.provider,
|
|
428
|
+
reasoningEffort: selection.reasoningEffort, variant: selection.variant, reason: decision?.reason ?? 'Invalid controller response; configured parent fallback',
|
|
429
|
+
next: rule?.escalateTo ?? 'independent gate verification', ...(ctx.story.assessment ? { assessment: ctx.story.assessment } : {}) });
|
|
430
|
+
let runner;
|
|
431
|
+
const execution = yield* perform(worker ? 'worker' : 'parent', provider, selection, () => runner(ctx), selected, () => {
|
|
432
|
+
runner = makeWorker(provider, selection);
|
|
433
|
+
if (bounded)
|
|
434
|
+
reserveWorker(provider, selection, selected, options.maxAttempts ?? 5);
|
|
435
|
+
});
|
|
436
|
+
if (execution.blocked)
|
|
437
|
+
return blocked(execution.blocked);
|
|
438
|
+
const result = execution.result;
|
|
439
|
+
const tokens = { ...aggregateCalls(calls), storyId: ctx.story.id, escalated: Boolean(rule && failedStories.has(failureKey)), ...(reservation ? { routingAttemptId: reservation.id } : {}) };
|
|
283
440
|
let recorded = false;
|
|
284
|
-
const recordOutcome = (verificationSuccess) => {
|
|
441
|
+
const recordOutcome = (verificationSuccess, failureKind) => {
|
|
285
442
|
if (recorded)
|
|
286
443
|
return;
|
|
444
|
+
if (reservation)
|
|
445
|
+
finishRoutingAttempt(root, reservation, { verificationSuccess, failureKind, actualModel: result.tokens?.model });
|
|
287
446
|
recorded = true;
|
|
288
|
-
if (!verificationSuccess)
|
|
289
|
-
failedStories.add(
|
|
290
|
-
else
|
|
291
|
-
failedStories.delete(
|
|
292
|
-
const project = projectHash(
|
|
293
|
-
recordRoutingObservation({
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
...(selection.reasoningEffort ? { requestedReasoningEffort: selection.reasoningEffort } : {}),
|
|
301
|
-
...(selection.provider ? { requestedProvider: selection.provider } : {}),
|
|
302
|
-
...(selection.variant ? { requestedVariant: selection.variant } : {}),
|
|
303
|
-
...(result.tokens?.model ? { actualModel: result.tokens.model } : {}),
|
|
304
|
-
orchestratorProvider: options.parent,
|
|
305
|
-
...(orchestratorSelection.model ? { orchestratorModel: orchestratorSelection.model } : {}),
|
|
306
|
-
orchestratorDurationMs,
|
|
307
|
-
workerDurationMs,
|
|
308
|
-
processSuccess: result.success,
|
|
309
|
-
verificationSuccess,
|
|
310
|
-
inputTokens: tokens.inputTokens,
|
|
311
|
-
outputTokens: tokens.outputTokens,
|
|
312
|
-
});
|
|
447
|
+
if (!verificationSuccess && failureKind !== 'infrastructure')
|
|
448
|
+
failedStories.add(failureKey);
|
|
449
|
+
else if (verificationSuccess)
|
|
450
|
+
failedStories.delete(failureKey);
|
|
451
|
+
const project = projectHash(root);
|
|
452
|
+
recordRoutingObservation({ projectHash: project, storyHash: storyHash(project, ctx.story.id), strategy: options.strategy, selected, provider,
|
|
453
|
+
requestedModel: selection.model, requestedReasoningEffort: selection.reasoningEffort, requestedProvider: selection.provider, requestedVariant: selection.variant,
|
|
454
|
+
actualModel: result.tokens?.model, orchestratorProvider: options.parent, orchestratorModel: orchestratorSelection.model,
|
|
455
|
+
orchestratorDurationMs: calls.filter(call => call.role === 'orchestrator').reduce((sum, call) => sum + call.durationMs, 0), workerDurationMs: calls.at(-1).durationMs,
|
|
456
|
+
processSuccess: result.success, verificationSuccess, failureKind,
|
|
457
|
+
inputTokens: tokens.inputTokens, outputTokens: tokens.outputTokens, usageAvailable: tokens.measurementComplete,
|
|
458
|
+
totalCostUsd: tokens.totalCostUsd, costMeasurementComplete: tokens.costMeasurementComplete, });
|
|
313
459
|
};
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
return { ...result, summary: `${routeSummary}; ${result.summary}`, tokens, routing: { recordOutcome } }
|
|
460
|
+
if (result.infrastructureFailure)
|
|
461
|
+
recordOutcome(false, 'infrastructure');
|
|
462
|
+
const routeSummary = decision ? `route=${selected} (${decision.reason})` : `route=SELF (${routeRun.success ? 'invalid routing response' : 'orchestrator failed'})`;
|
|
463
|
+
return { ...result, summary: `${routeSummary}; ${result.summary}`, tokens, routing: { recordOutcome, ...(result.infrastructureFailure ? { blocked: true } : {}),
|
|
464
|
+
...(bounded ? { canRetry: !result.infrastructureFailure && reservation.ordinal < (options.maxAttempts ?? 5) } : {}) } };
|
|
318
465
|
};
|
|
319
466
|
}
|
|
320
467
|
function aggregateCalls(calls) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
468
|
+
const optional = {};
|
|
469
|
+
for (const field of ['cachedInputTokens', 'cacheWriteInputTokens', 'reasoningOutputTokens', 'totalCostUsd']) {
|
|
470
|
+
if (calls.some(call => call[field] !== undefined))
|
|
471
|
+
optional[field] = calls.reduce((sum, call) => sum + (call[field] ?? 0), 0);
|
|
472
|
+
}
|
|
473
|
+
const model = calls.at(-1)?.actualModel;
|
|
474
|
+
return { inputTokens: calls.reduce((sum, call) => sum + call.inputTokens, 0), outputTokens: calls.reduce((sum, call) => sum + call.outputTokens, 0),
|
|
475
|
+
...optional, ...(model ? { model } : {}), calls,
|
|
476
|
+
measurementComplete: calls.every(call => call.usageAvailable !== false),
|
|
477
|
+
costMeasurementComplete: calls.every(call => call.totalCostUsd !== undefined && call.costMeasurementComplete !== false) };
|
|
325
478
|
}
|
package/dist/setup/command.js
CHANGED
|
@@ -46,7 +46,10 @@ export function defaultRoutingWorkers(agents) {
|
|
|
46
46
|
{ id: 'hermes-standard', agent: 'hermes', tier: 'standard', costTier: 'medium', capabilities: ['implementation'] },
|
|
47
47
|
],
|
|
48
48
|
};
|
|
49
|
-
return agents.flatMap(agent => workers[agent])
|
|
49
|
+
return agents.flatMap(agent => workers[agent]).map(worker => ({
|
|
50
|
+
...worker,
|
|
51
|
+
profileMetadata: { version: 1, catalog: 'yoke-1.22.0', source: 'yoke-default', basis: 'configured-prior' },
|
|
52
|
+
}));
|
|
50
53
|
}
|
|
51
54
|
function parseAgents(value, fallback) {
|
|
52
55
|
if (value.trim().toLowerCase() === 'all')
|
|
@@ -55,6 +58,8 @@ function parseAgents(value, fallback) {
|
|
|
55
58
|
return parsed.length > 0 ? [...new Set(parsed)] : fallback;
|
|
56
59
|
}
|
|
57
60
|
function yes(value, fallback) {
|
|
61
|
+
if (!value)
|
|
62
|
+
return fallback;
|
|
58
63
|
const normalized = value.trim().toLowerCase();
|
|
59
64
|
if (['y', 'yes', 'j', 'ja', 'true', '1'].includes(normalized))
|
|
60
65
|
return true;
|
|
@@ -99,6 +104,10 @@ export async function runSetup(targetDir, opts = {}) {
|
|
|
99
104
|
let runner = defaultRunner;
|
|
100
105
|
let decisionPolicy = defaultPolicy;
|
|
101
106
|
let routing = defaultRouting;
|
|
107
|
+
let runnerModel = opts.runnerModel ?? existing?.runner?.model;
|
|
108
|
+
let runnerReasoning = opts.runnerReasoning ?? existing?.runner?.reasoningEffort;
|
|
109
|
+
const agentModels = { ...(opts.agentModels ?? {}) };
|
|
110
|
+
const agentReasoning = { ...(opts.agentReasoning ?? {}) };
|
|
102
111
|
if (interactive && ask) {
|
|
103
112
|
agents = parseAgents(await ask(`Agents [${defaultAgents.join(',')}] (${SUPPORTED_AGENTS.join(',')}|all): `), defaultAgents);
|
|
104
113
|
const graphAnswer = (await ask(`Code graph [${defaultGraph}] (graphify|serena): `)).trim().toLowerCase();
|
|
@@ -112,12 +121,39 @@ export async function runSetup(targetDir, opts = {}) {
|
|
|
112
121
|
if (policyAnswer === 'auto' || policyAnswer === 'critical')
|
|
113
122
|
decisionPolicy = policyAnswer;
|
|
114
123
|
routing = yes(await ask(`Enable adaptive multi-model routing? [${defaultRouting ? 'yes' : 'no'}]: `), defaultRouting);
|
|
124
|
+
if (opts.configureModels) {
|
|
125
|
+
const configureModels = yes(await ask(`Configure model & reasoning effort per agent? [no]: `), false);
|
|
126
|
+
if (configureModels) {
|
|
127
|
+
const rm = (await ask(`Runner model [${runnerModel ?? 'default'}]: `)).trim();
|
|
128
|
+
if (rm)
|
|
129
|
+
runnerModel = rm;
|
|
130
|
+
const rr = (await ask(`Runner reasoning effort (none|low|medium|high|xhigh) [${runnerReasoning ?? 'default'}]: `)).trim();
|
|
131
|
+
if (rr)
|
|
132
|
+
runnerReasoning = rr;
|
|
133
|
+
for (const a of agents) {
|
|
134
|
+
const am = (await ask(`Model for agent "${a}" [${agentModels[a] ?? 'default'}]: `)).trim();
|
|
135
|
+
if (am)
|
|
136
|
+
agentModels[a] = am;
|
|
137
|
+
const ar = (await ask(`Reasoning effort for agent "${a}" (none|low|medium|high|xhigh) [${agentReasoning[a] ?? 'default'}]: `)).trim();
|
|
138
|
+
if (ar)
|
|
139
|
+
agentReasoning[a] = ar;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
115
143
|
}
|
|
116
144
|
if (modelProviders.length && !agents.includes('qwen'))
|
|
117
145
|
agents = [...agents, 'qwen'];
|
|
118
146
|
if (!agents.includes(runner))
|
|
119
147
|
agents = [...agents, runner];
|
|
120
|
-
const
|
|
148
|
+
const priorRunner = existing?.runner?.agent ?? existing?.agents[0];
|
|
149
|
+
const runnerChanged = priorRunner !== undefined && priorRunner !== runner;
|
|
150
|
+
if (runnerChanged) {
|
|
151
|
+
if (opts.runnerModel === undefined)
|
|
152
|
+
runnerModel = undefined;
|
|
153
|
+
if (opts.runnerReasoning === undefined)
|
|
154
|
+
runnerReasoning = undefined;
|
|
155
|
+
}
|
|
156
|
+
const code = runRetrofit(targetDir, { loop, agents, codeGraph, codeIntelligence, host, cleanWorktrees: opts.cleanWorktrees, runnerModel, runnerReasoning, agentModels, agentReasoning });
|
|
121
157
|
if (code !== 0)
|
|
122
158
|
return code;
|
|
123
159
|
applyActions(presetActions, targetDir, { backupDir: join(targetDir, '.yoke', 'backups', `model-presets-${Date.now()}`) });
|
|
@@ -125,14 +161,25 @@ export async function runSetup(targetDir, opts = {}) {
|
|
|
125
161
|
if (!config)
|
|
126
162
|
return 1;
|
|
127
163
|
config.loop = { parallel: 'auto', isolate: true, ...config.loop, enabled: loop, decisionPolicy };
|
|
128
|
-
const priorRunner = existing?.runner?.agent ?? existing?.agents[0];
|
|
129
164
|
const selectPresetModel = presetWorkers.length > 0 && runner === 'qwen' && (!existing || (priorRunner !== undefined && priorRunner !== runner));
|
|
130
165
|
if (selectPresetModel && priorRunner !== 'qwen')
|
|
131
166
|
config.runner = { permissions: config.runner?.permissions };
|
|
132
|
-
config.runner = {
|
|
167
|
+
config.runner = {
|
|
168
|
+
...config.runner,
|
|
169
|
+
agent: runner,
|
|
170
|
+
...(runnerModel ? { model: runnerModel } : selectPresetModel && !config.runner?.model ? { model: presetWorkers[0].model } : {}),
|
|
171
|
+
...(runnerReasoning ? { reasoningEffort: runnerReasoning } : {}),
|
|
172
|
+
};
|
|
133
173
|
const existingWorkers = config.routing?.workers ?? [];
|
|
134
174
|
const baseWorkers = existingWorkers.length > 0 && !opts.routingPreset ? existingWorkers : defaultRoutingWorkers(agents).filter(worker => !modelProviders.length || worker.agent !== 'qwen');
|
|
135
|
-
const
|
|
175
|
+
const rawWorkers = [...baseWorkers, ...presetWorkers.filter(worker => !baseWorkers.some(existing => existing.id === worker.id))];
|
|
176
|
+
const workers = rawWorkers.map(w => {
|
|
177
|
+
const model = agentModels[w.agent] ?? w.model;
|
|
178
|
+
const reasoningEffort = agentReasoning[w.agent] ?? w.reasoningEffort;
|
|
179
|
+
return { ...w, ...(model ? { model } : {}), ...(reasoningEffort ? { reasoningEffort } : {}),
|
|
180
|
+
...((agentModels[w.agent] || agentReasoning[w.agent]) && w.profileMetadata ? { profileMetadata: { ...w.profileMetadata, source: 'operator' } } : {}),
|
|
181
|
+
};
|
|
182
|
+
});
|
|
136
183
|
config.routing = {
|
|
137
184
|
...config.routing,
|
|
138
185
|
enabled: routing,
|
|
@@ -140,12 +187,13 @@ export async function runSetup(targetDir, opts = {}) {
|
|
|
140
187
|
maxCandidates: config.routing?.maxCandidates ?? 3,
|
|
141
188
|
assessmentPolicy: existing?.routing?.assessmentPolicy ?? (existing ? 'on-demand' : 'prepared'),
|
|
142
189
|
fallback: existing?.routing?.fallback ?? (existing ? 'parent' : 'block'),
|
|
190
|
+
...(!existing && !config.routing?.optimization ? { optimization: { version: 1, objective: 'balanced', minSamples: 20 } } : {}),
|
|
143
191
|
...(config.routing?.orchestrator ? { orchestrator: config.routing.orchestrator } : {}),
|
|
144
192
|
workers,
|
|
145
193
|
};
|
|
146
194
|
YokeConfigSchema.parse(config);
|
|
147
195
|
saveConfig(targetDir, config);
|
|
148
|
-
console.log(`Yoke setup complete: agents=${agents.join(',')} · runner=${runner} · loop=${loop ? 'on' : 'off'} · routing=${routing ? 'on' : 'off'} · decisions=${decisionPolicy}`);
|
|
196
|
+
console.log(`Yoke setup complete: agents=${agents.join(',')} · runner=${runner}${runnerModel ? ` (${runnerModel})` : ''}${runnerReasoning ? ` [${runnerReasoning}]` : ''} · loop=${loop ? 'on' : 'off'} · routing=${routing ? 'on' : 'off'} · decisions=${decisionPolicy}`);
|
|
149
197
|
return 0;
|
|
150
198
|
}
|
|
151
199
|
finally {
|