@kienntpixon/dieu-phoi 0.2.2 → 0.4.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/CHANGELOG.md +29 -0
- package/README.md +10 -5
- package/bin/dieu-phoi.cjs +11 -3
- package/hook.cjs +258 -11
- package/lib/aggregate.cjs +72 -13
- package/lib/choose.cjs +74 -18
- package/lib/commands.cjs +258 -34
- package/lib/setup.cjs +8 -4
- package/package.json +1 -1
package/lib/aggregate.cjs
CHANGED
|
@@ -9,6 +9,12 @@ const core = require('../hook.cjs');
|
|
|
9
9
|
const MIN_N = 5;
|
|
10
10
|
const FAIL_OUTCOMES = new Set(['fail', 'timeout']);
|
|
11
11
|
const HEAVY_STEERS = new Set(['plan', 'redirect']);
|
|
12
|
+
// core 0.4.0: an agent stopped by a usage-limit/quota pause is `aborted` — neither pass nor fail. Legacy
|
|
13
|
+
// rows recorded such pauses as `timeout` with a quota-ish reason_code; they are read as `aborted` too.
|
|
14
|
+
const ABORTED = 'aborted';
|
|
15
|
+
// Exact markers only (anchored): `quota-pause`, `usage-limit`, the hook's `auto-*` codes and the legacy
|
|
16
|
+
// `paused-for-quota-*` codes the orchestrator wrote before 0.4.0. A reason that merely mentions a rate limit stays a timeout.
|
|
17
|
+
const QUOTA_REASON_RE = /^(?:quota-pause|usage-limit|auto-quota-pause|auto-usage-limit|paused-for-quota)(?:-|$)/;
|
|
12
18
|
|
|
13
19
|
function tsOf(ev) {
|
|
14
20
|
const t = Date.parse(ev && ev.ts);
|
|
@@ -56,7 +62,33 @@ function normalizeVerdict(ev) {
|
|
|
56
62
|
const by = ev.by === 'user' ? 'user' : 'agent';
|
|
57
63
|
let cause = ev.cause || null;
|
|
58
64
|
if (by === 'user' && ev.outcome === 'reject' && cause !== 'scope') cause = 'quality';
|
|
59
|
-
|
|
65
|
+
let outcome = ev.outcome;
|
|
66
|
+
if (by === 'agent' && outcome === 'timeout' && typeof ev.reason_code === 'string' && QUOTA_REASON_RE.test(ev.reason_code)) {
|
|
67
|
+
outcome = ABORTED;
|
|
68
|
+
}
|
|
69
|
+
return { by, outcome, cause, round: Number(ev.round) || null };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Task ids withdrawn with `dieu-phoi discard --task` (kind=discard). Every event of such a task — and every
|
|
74
|
+
* event of an agent dispatched for it — is left out of tasks, cells and steers (CONTRACT "Sự kiện cục bộ").
|
|
75
|
+
*/
|
|
76
|
+
function discardedTaskIds(events) {
|
|
77
|
+
const out = new Set();
|
|
78
|
+
for (const ev of events) if (ev && ev.kind === 'discard' && typeof ev.task_id === 'string' && ev.task_id) out.add(ev.task_id);
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Events with discarded tasks (and the discard records themselves) replaced by null; order kept. */
|
|
83
|
+
function withoutDiscarded(events) {
|
|
84
|
+
const gone = discardedTaskIds(events);
|
|
85
|
+
const agents = new Set(events.filter((ev) => ev && ev.kind === 'dispatch' && gone.has(ev.task_id)).map((ev) => ev.agent_id));
|
|
86
|
+
return events.map((ev) => {
|
|
87
|
+
if (!ev || typeof ev !== 'object' || ev.kind === 'discard') return null;
|
|
88
|
+
if (gone.has(ev.task_id) || agents.has(ev.agent_id)) return null;
|
|
89
|
+
if (typeof ev.agent_id === 'string' && ev.agent_id.startsWith('task:') && gone.has(ev.agent_id.slice(5))) return null;
|
|
90
|
+
return ev;
|
|
91
|
+
});
|
|
60
92
|
}
|
|
61
93
|
|
|
62
94
|
function blank(v) {
|
|
@@ -111,7 +143,9 @@ function buildAgents(events) {
|
|
|
111
143
|
const d = a.dispatch || {};
|
|
112
144
|
const c0 = a.completes[0] || {};
|
|
113
145
|
const cl = a.completes[a.completes.length - 1] || {};
|
|
114
|
-
const
|
|
146
|
+
const vAll = a.verdicts.slice().sort((x, y) => tsOf(x) - tsOf(y) || x._idx - y._idx);
|
|
147
|
+
// `aborted` (quota pause) is neither pass nor fail: it never sets the outcome nor counts as a result.
|
|
148
|
+
const v = vAll.filter((x) => x.outcome !== ABORTED);
|
|
115
149
|
const verdict = v.length ? v[v.length - 1] : null;
|
|
116
150
|
const firstVerdictTs = v.length ? tsOf(v[0]) : Infinity;
|
|
117
151
|
const pick = (k) => (d[k] !== undefined && d[k] !== null ? d[k] : (c0[k] !== undefined && c0[k] !== null ? c0[k] : null));
|
|
@@ -137,6 +171,9 @@ function buildAgents(events) {
|
|
|
137
171
|
tool_uses: a.completes.length ? sumOrNull(a.completes.map((x) => x.tool_uses)) : null,
|
|
138
172
|
outcome: verdict ? verdict.outcome : null,
|
|
139
173
|
first_outcome: v.length ? v[0].outcome : null,
|
|
174
|
+
// every non-aborted agent verdict in time order: a retry by SendMessage to the same agent is a new result
|
|
175
|
+
verdict_seq: v.map((x) => ({ outcome: x.outcome, ts: tsOf(x), idx: x._idx })),
|
|
176
|
+
aborted: vAll.length > 0 && vAll[vAll.length - 1].outcome === ABORTED,
|
|
140
177
|
verify: verdict ? verdict.verify : null,
|
|
141
178
|
reason_code: verdict ? verdict.reason_code : null,
|
|
142
179
|
steers: a.steers.map((s) => s.steer_kind).filter(Boolean),
|
|
@@ -175,16 +212,25 @@ function buildTasks(agents, userVerdicts) {
|
|
|
175
212
|
const first = t.agents.filter((a) => a.role === 'exec').sort(byTry)[0] || null;
|
|
176
213
|
const user = t.user.slice().sort((x, y) => ((x.round || 0) - (y.round || 0)) || (tsOf(x) - tsOf(y)) || (x._idx - y._idx));
|
|
177
214
|
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
215
|
+
// Results for escalation. Each agent verdict of an attempt is one result at its tier (a retry by
|
|
216
|
+
// SendMessage to the same agent is a new result); a user verdict adds a result to the latest attempt
|
|
217
|
+
// dispatched before it (reject/quality = fail, accept = pass). eff_outcome = the attempt's latest result.
|
|
218
|
+
for (const a of attempts) {
|
|
219
|
+
a.results = (a.verdict_seq || [])
|
|
220
|
+
.filter((x) => x.outcome === 'pass' || FAIL_OUTCOMES.has(x.outcome))
|
|
221
|
+
.map((x) => ({ outcome: x.outcome === 'pass' ? 'pass' : 'fail', ts: x.ts, idx: x.idx }));
|
|
222
|
+
a.eff_outcome = a.results.length ? a.results[a.results.length - 1].outcome : null;
|
|
223
|
+
}
|
|
181
224
|
for (const v of user) {
|
|
182
225
|
if (v.cause === 'scope') continue;
|
|
183
226
|
const vt = tsOf(v);
|
|
184
227
|
let target = null;
|
|
185
228
|
for (const a of attempts) if (a.ts <= vt && (!target || a.ts >= target.ts)) target = a;
|
|
186
229
|
if (!target) target = attempts[attempts.length - 1] || null;
|
|
187
|
-
if (target)
|
|
230
|
+
if (target) {
|
|
231
|
+
target.eff_outcome = v.outcome === 'accept' ? 'pass' : 'fail';
|
|
232
|
+
target.results.push({ outcome: target.eff_outcome, ts: vt, idx: v._idx, user: true });
|
|
233
|
+
}
|
|
188
234
|
}
|
|
189
235
|
|
|
190
236
|
const firstUser = user[0] || null;
|
|
@@ -307,13 +353,19 @@ function canonicalCells(cells) {
|
|
|
307
353
|
return cells.map((c) => Object.fromEntries(CANONICAL_CELL_FIELDS.map((k) => [k, c[k] === undefined ? null : c[k]])));
|
|
308
354
|
}
|
|
309
355
|
|
|
310
|
-
/**
|
|
356
|
+
/**
|
|
357
|
+
* Steering per task_type, counted in distinct user actions (steer_id), not events. Core 0.4.0: only
|
|
358
|
+
* exec-phase steers count — a steer on a role=exec agent, or a task-level steer on a task that has an exec
|
|
359
|
+
* attempt. Steers on scouts/planners (gate-1 Q&A) and on verifiers go to `plan_phase_actions` and never
|
|
360
|
+
* touch actions / steer_rate.
|
|
361
|
+
*/
|
|
311
362
|
function buildSteers(tasks, steers, agents) {
|
|
312
363
|
const typeOfTask = new Map(tasks.map((t) => [t.task_id, t.task_type]));
|
|
364
|
+
const execTask = new Set(tasks.filter((t) => t.first && t.task_id).map((t) => t.task_id));
|
|
313
365
|
const agentById = new Map(agents.map((a) => [a.agent_id, a]));
|
|
314
366
|
const by = {};
|
|
315
367
|
const row = (type) => by[type] || (by[type] = {
|
|
316
|
-
task_type: type, tasks: 0, steered_tasks: 0, actions: 0, minor: 0, plan: 0, redirect: 0, _tasks: new Set(), _ids: new Set(),
|
|
368
|
+
task_type: type, tasks: 0, steered_tasks: 0, actions: 0, minor: 0, plan: 0, redirect: 0, _tasks: new Set(), _ids: new Set(), _plan: new Set(),
|
|
317
369
|
});
|
|
318
370
|
for (const t of tasks) if (t.first) row(t.task_type).tasks++;
|
|
319
371
|
for (const s of steers) {
|
|
@@ -322,6 +374,8 @@ function buildSteers(tasks, steers, agents) {
|
|
|
322
374
|
const type = (taskId && typeOfTask.get(taskId)) || s.task_type || (a && a.task_type) || 'untagged';
|
|
323
375
|
const r = row(type);
|
|
324
376
|
const id = steerIdOf(s);
|
|
377
|
+
const execPhase = a ? a.role === 'exec' : !(taskId && typeOfTask.has(taskId) && !execTask.has(taskId));
|
|
378
|
+
if (!execPhase) { r._plan.add(id); continue; }
|
|
325
379
|
if (r._ids.has(id)) continue;
|
|
326
380
|
r._ids.add(id);
|
|
327
381
|
r.actions++;
|
|
@@ -334,6 +388,7 @@ function buildSteers(tasks, steers, agents) {
|
|
|
334
388
|
task_type: r.task_type, tasks: r.tasks, steered_tasks: steered, actions: r.actions,
|
|
335
389
|
minor: r.minor, plan: r.plan, redirect: r.redirect,
|
|
336
390
|
steer_rate: r.tasks ? round6(Math.min(1, steered / r.tasks)) : 0,
|
|
391
|
+
plan_phase_actions: Array.from(r._plan).filter((x) => !r._ids.has(x)).length,
|
|
337
392
|
};
|
|
338
393
|
return outRow;
|
|
339
394
|
}).sort((a, b) => b.steer_rate - a.steer_rate || a.task_type.localeCompare(b.task_type));
|
|
@@ -345,7 +400,8 @@ function finiteNum(v) {
|
|
|
345
400
|
|
|
346
401
|
/**
|
|
347
402
|
* Orchestration cost (CONTRACT "Thước đo" mục 6). Sessions = distinct non-empty `session_id` over
|
|
348
|
-
* dispatch events. Per session: ctx = max orch_ctx_tokens, usd = max orch_cum_usd
|
|
403
|
+
* dispatch events. Per session: ctx = max orch_ctx_tokens, usd = max orch_cum_usd minus orch_cum_usd of the
|
|
404
|
+
* session's first dispatch carrying a number (core 0.4.0; never negative), growth = max of
|
|
349
405
|
* max(0, orch_ctx_tokens - orch_ctx_baseline) over dispatches carrying both, comp = max orch_compactions
|
|
350
406
|
* (numbers only). compactions_total = sum of per-session comp, compactions_max = max.
|
|
351
407
|
* Each agent belongs to the session of its FIRST dispatch (file order); sub_usd_total sums the
|
|
@@ -357,11 +413,14 @@ function orchestrationSummary(events) {
|
|
|
357
413
|
for (const ev of events) {
|
|
358
414
|
if (!ev || ev.kind !== 'dispatch' || typeof ev.session_id !== 'string' || !ev.session_id) continue;
|
|
359
415
|
const sid = ev.session_id;
|
|
360
|
-
if (!sessions.has(sid)) sessions.set(sid, { ctx: null, usd: null, growth: null, comp: null });
|
|
416
|
+
if (!sessions.has(sid)) sessions.set(sid, { ctx: null, usd: null, base: null, growth: null, comp: null });
|
|
361
417
|
const s = sessions.get(sid);
|
|
362
418
|
const c = finiteNum(ev.orch_ctx_tokens);
|
|
363
419
|
const u = finiteNum(ev.orch_cum_usd);
|
|
364
420
|
if (c !== null && (s.ctx === null || c > s.ctx)) s.ctx = c;
|
|
421
|
+
// core 0.4.0: orchestrator cost counts from the session's FIRST dispatch carrying a number (file order),
|
|
422
|
+
// not from session start — whatever the session did before it started dispatching is not orchestration.
|
|
423
|
+
if (u !== null && s.base === null) s.base = u;
|
|
365
424
|
if (u !== null && (s.usd === null || u > s.usd)) s.usd = u;
|
|
366
425
|
const k = finiteNum(ev.orch_compactions);
|
|
367
426
|
if (k !== null && (s.comp === null || k > s.comp)) s.comp = k;
|
|
@@ -386,7 +445,7 @@ function orchestrationSummary(events) {
|
|
|
386
445
|
if (s.ctx !== null) ctxs.push(s.ctx);
|
|
387
446
|
if (s.growth !== null) growths.push(s.growth);
|
|
388
447
|
if (s.comp !== null) comps.push(s.comp);
|
|
389
|
-
if (s.usd !== null) orch += s.usd;
|
|
448
|
+
if (s.usd !== null) orch += Math.max(0, s.usd - s.base);
|
|
390
449
|
}
|
|
391
450
|
return {
|
|
392
451
|
sessions: sessions.size,
|
|
@@ -403,7 +462,7 @@ function orchestrationSummary(events) {
|
|
|
403
462
|
}
|
|
404
463
|
|
|
405
464
|
function analyze(events, opts) {
|
|
406
|
-
const { agents, userVerdicts, overrides, steers } = buildAgents(events);
|
|
465
|
+
const { agents, userVerdicts, overrides, steers } = buildAgents(withoutDiscarded(events));
|
|
407
466
|
const tasks = buildTasks(agents, userVerdicts);
|
|
408
467
|
const cells = buildCells(tasks, opts);
|
|
409
468
|
return {
|
|
@@ -413,6 +472,6 @@ function analyze(events, opts) {
|
|
|
413
472
|
}
|
|
414
473
|
|
|
415
474
|
module.exports = {
|
|
416
|
-
MIN_N, FAIL_OUTCOMES, HEAVY_STEERS, CANONICAL_CELL_FIELDS, normalizeVerdict, isInternalComplete, buildAgents, buildTasks,
|
|
475
|
+
MIN_N, FAIL_OUTCOMES, HEAVY_STEERS, ABORTED, QUOTA_REASON_RE, discardedTaskIds, withoutDiscarded, CANONICAL_CELL_FIELDS, normalizeVerdict, isInternalComplete, buildAgents, buildTasks,
|
|
417
476
|
buildCells, buildSteers, canonicalCells, orchestrationSummary, analyze, cellKey, mean, percentile, tsOf, steerIdOf,
|
|
418
477
|
};
|
package/lib/choose.cjs
CHANGED
|
@@ -19,7 +19,8 @@ const LADDER = [
|
|
|
19
19
|
const TOP = LADDER.length - 1;
|
|
20
20
|
const EFFORT_RANK = { low: 0, medium: 1, high: 2, xhigh: 3, max: 4 };
|
|
21
21
|
|
|
22
|
-
// Built-in defaults = skill/profile/profile.json (
|
|
22
|
+
// Built-in defaults = skill/profile/profile.json for the fixed task types (time caps as tuned 2026-10-12: feature-impl 80,
|
|
23
|
+
// content-write 40, debug 55 — the old ones sat below the observed p50); used only when no profile is found.
|
|
23
24
|
const DEFAULT_PRIORS = {
|
|
24
25
|
search: 'haiku/medium',
|
|
25
26
|
extract: 'haiku/medium',
|
|
@@ -38,13 +39,13 @@ const DEFAULT_TIME_CAP_MIN = {
|
|
|
38
39
|
search: 3,
|
|
39
40
|
extract: 5,
|
|
40
41
|
'mechanical-edit': 10,
|
|
41
|
-
'feature-impl':
|
|
42
|
-
debug:
|
|
42
|
+
'feature-impl': 80,
|
|
43
|
+
debug: 55,
|
|
43
44
|
refactor: 30,
|
|
44
45
|
'test-write': 15,
|
|
45
46
|
review: 10,
|
|
46
47
|
'research-synth': 20,
|
|
47
|
-
'content-write':
|
|
48
|
+
'content-write': 40,
|
|
48
49
|
'plan-design': 15,
|
|
49
50
|
'data-analysis': 15,
|
|
50
51
|
};
|
|
@@ -187,25 +188,35 @@ const OPUS_MEDIUM = 5;
|
|
|
187
188
|
* - one fail at a tier -> same tier again (retry with the error);
|
|
188
189
|
* - two fails at the same tier -> next tier up;
|
|
189
190
|
* - if that next tier would exceed the opus/medium ceiling -> ask the user (no auto re-plan).
|
|
190
|
-
* Fail = agent fail|timeout or user reject cause=quality
|
|
191
|
-
*
|
|
191
|
+
* Fail = agent fail|timeout or user reject cause=quality. Core 0.4.0: every result counts, so a retry by
|
|
192
|
+
* SendMessage to the same agent (fail, then fail again) is two fails at that tier; `aborted` (quota pause)
|
|
193
|
+
* is never a result. Attempts steered plan/redirect by the user are ignored.
|
|
192
194
|
*/
|
|
193
195
|
function escalation(task, role) {
|
|
194
196
|
const none = { floor: 0, ask: false, note: null, fails: 0, tried: [] };
|
|
195
197
|
if (!task) return none;
|
|
196
198
|
const att = task.attempts
|
|
197
|
-
.filter((a) => a.
|
|
198
|
-
.map((a) => ({
|
|
199
|
-
|
|
199
|
+
.filter((a) => a.role !== 'verify' && !a.steered)
|
|
200
|
+
.map((a) => ({
|
|
201
|
+
a,
|
|
202
|
+
i: tierIndex(a.model, a.effort),
|
|
203
|
+
results: Array.isArray(a.results) ? a.results : (a.eff_outcome ? [{ outcome: a.eff_outcome, ts: a.ts, idx: 0 }] : []),
|
|
204
|
+
}))
|
|
205
|
+
.filter((x) => x.i !== null && x.results.length)
|
|
200
206
|
.sort((x, y) => (x.a.try - y.a.try) || (x.a.ts - y.a.ts));
|
|
201
207
|
if (!att.length) return none;
|
|
208
|
+
const res = [];
|
|
209
|
+
for (const x of att) {
|
|
210
|
+
const rs = x.results.slice().sort((p, q) => (p.ts - q.ts) || ((p.idx || 0) - (q.idx || 0)));
|
|
211
|
+
for (const r of rs) res.push({ i: x.i, outcome: r.outcome });
|
|
212
|
+
}
|
|
202
213
|
const tried = Array.from(new Set(att.map((x) => tierName(x.i))));
|
|
203
|
-
const totalFails =
|
|
204
|
-
const last =
|
|
214
|
+
const totalFails = res.filter((r) => r.outcome === 'fail').length;
|
|
215
|
+
const last = res[res.length - 1];
|
|
205
216
|
const cur = last.i;
|
|
206
217
|
const base = { tried, fails: totalFails, ask: false };
|
|
207
|
-
if (last.
|
|
208
|
-
const fails =
|
|
218
|
+
if (last.outcome !== 'fail') return Object.assign(base, { floor: cur, note: null });
|
|
219
|
+
const fails = res.filter((r) => r.i === cur && r.outcome === 'fail').length;
|
|
209
220
|
if (fails < 2) {
|
|
210
221
|
return Object.assign(base, { floor: cur, note: `task fail 1 lần ở ${tierName(cur)} → làm lại cùng tier kèm lỗi` });
|
|
211
222
|
}
|
|
@@ -287,6 +298,40 @@ function estimateFor(idx, cell, pricing) {
|
|
|
287
298
|
return roughEstimate(idx, pricing);
|
|
288
299
|
}
|
|
289
300
|
|
|
301
|
+
// ------------------------------------------------------------------ task type guard
|
|
302
|
+
|
|
303
|
+
// Ad-hoc labels seen in real logs → the closest fixed type (core 0.4.0). A label maps only when its target
|
|
304
|
+
// is known (fixed list or the profile's extra_task_types), e.g. art-fix → art-gen needs art-gen in the profile.
|
|
305
|
+
const TYPE_ALIASES = {
|
|
306
|
+
'bug-fix': 'debug', bugfix: 'debug', fix: 'debug', bug: 'debug',
|
|
307
|
+
config: 'mechanical-edit', shell: 'mechanical-edit', deploy: 'mechanical-edit', ops: 'mechanical-edit', edit: 'mechanical-edit',
|
|
308
|
+
docs: 'content-write', doc: 'content-write', documentation: 'content-write', writing: 'content-write', copy: 'content-write',
|
|
309
|
+
design: 'plan-design', plan: 'plan-design', planning: 'plan-design', architecture: 'plan-design',
|
|
310
|
+
research: 'research-synth', synth: 'research-synth',
|
|
311
|
+
test: 'test-write', tests: 'test-write',
|
|
312
|
+
analysis: 'data-analysis', data: 'data-analysis',
|
|
313
|
+
impl: 'feature-impl', feature: 'feature-impl', implement: 'feature-impl',
|
|
314
|
+
'code-review': 'review',
|
|
315
|
+
'art-fix': 'art-gen', art: 'art-gen',
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* { type, known, mapped_from, warning }: a fixed/extra type passes; an ad-hoc label with a known alias target
|
|
320
|
+
* maps to it (with a warning); anything else stays unknown (choose asks the user to pick a fixed type).
|
|
321
|
+
*/
|
|
322
|
+
function resolveTaskType(raw, profile) {
|
|
323
|
+
const t = String(raw || '').toLowerCase();
|
|
324
|
+
const extra = (profile && profile.extra_task_types) || [];
|
|
325
|
+
const isKnown = (x) => core.TASK_TYPES.includes(x) || extra.includes(x);
|
|
326
|
+
if (isKnown(t)) return { type: t, known: true, mapped_from: null, warning: null };
|
|
327
|
+
const target = TYPE_ALIASES[t];
|
|
328
|
+
if (target && isKnown(target)) {
|
|
329
|
+
return { type: target, known: true, mapped_from: t, warning: `loại "${t}" không cố định → dùng "${target}" (ghi --type ${target} trong thẻ)` };
|
|
330
|
+
}
|
|
331
|
+
const list = core.TASK_TYPES.concat(extra).join(', ');
|
|
332
|
+
return { type: t, known: false, mapped_from: null, warning: `loại "${t}" không có trong danh sách cố định (${list}) — chọn loại gần nhất hoặc hỏi người dùng` };
|
|
333
|
+
}
|
|
334
|
+
|
|
290
335
|
// ------------------------------------------------------------------ choose
|
|
291
336
|
|
|
292
337
|
/**
|
|
@@ -296,8 +341,8 @@ function estimateFor(idx, cell, pricing) {
|
|
|
296
341
|
* Throws when an override goes below a safety floor or off the ladder.
|
|
297
342
|
*/
|
|
298
343
|
function choose(opts) {
|
|
299
|
-
const
|
|
300
|
-
if (!
|
|
344
|
+
const rawType = String(opts.type || '').toLowerCase();
|
|
345
|
+
if (!rawType) throw new Error('Thiếu --type');
|
|
301
346
|
const score = Number(opts.score);
|
|
302
347
|
if (opts.score === undefined || opts.score === null || opts.score === true || !isFinite(score)
|
|
303
348
|
|| score < 0 || score > 12) throw new Error('--score phải trong 0–12');
|
|
@@ -327,12 +372,15 @@ function choose(opts) {
|
|
|
327
372
|
const notes = [];
|
|
328
373
|
|
|
329
374
|
const analysis = agg.analyze(opts.events || [], { lambda });
|
|
330
|
-
const
|
|
375
|
+
const rt = resolveTaskType(rawType, profile);
|
|
376
|
+
const type = rt.type;
|
|
377
|
+
const known = rt.known;
|
|
378
|
+
if (rt.warning) notes.push(rt.warning);
|
|
331
379
|
let prior = profile.priors[type];
|
|
332
380
|
let priorWhy = 'prior';
|
|
333
381
|
if (prior === undefined || prior === null) {
|
|
334
382
|
prior = OPUS_MEDIUM;
|
|
335
|
-
notes.push(
|
|
383
|
+
if (known) notes.push('không có prior, dùng opus/medium');
|
|
336
384
|
}
|
|
337
385
|
const ov = overridePrior(analysis.overrides, type);
|
|
338
386
|
if (ov && ov.tier !== prior) {
|
|
@@ -382,10 +430,17 @@ function choose(opts) {
|
|
|
382
430
|
explored,
|
|
383
431
|
tier: tierName(idx),
|
|
384
432
|
role,
|
|
433
|
+
task_type: type,
|
|
385
434
|
budget_s: budgetFor(cell),
|
|
386
|
-
}, estimateFor(idx, cell, opts.pricing), { profile_source: src },
|
|
435
|
+
}, estimateFor(idx, cell, opts.pricing), { profile_source: src },
|
|
436
|
+
rt.mapped_from ? { mapped_from: rt.mapped_from } : {}, rt.warning ? { type_warning: rt.warning } : {}, extra || {});
|
|
387
437
|
};
|
|
388
438
|
|
|
439
|
+
// Unknown task type (not fixed, not in the profile, no alias): its stats would never pool — ask first.
|
|
440
|
+
if (!known) {
|
|
441
|
+
return finish(Math.max(prior, safetyFloor), rt.warning, false, { action: 'ask_user', unknown_type: true });
|
|
442
|
+
}
|
|
443
|
+
|
|
389
444
|
if (overrideIdx !== null && overrideIdx < safetyFloor) {
|
|
390
445
|
throw new Error(`Từ chối override ${tierName(overrideIdx)}: dưới sàn an toàn ${tierName(safetyFloor)} (${floorWhy.join(', ')}).`);
|
|
391
446
|
}
|
|
@@ -474,4 +529,5 @@ module.exports = {
|
|
|
474
529
|
DEFAULT_ORCH_CTX_SOFT_CAP,
|
|
475
530
|
LADDER, DEFAULT_PRIORS, DEFAULT_LAMBDA, DEFAULT_TIME_CAP_MIN, ROUGH, tierIndex, tierName, parsePrior,
|
|
476
531
|
parseTier, loadProfile, makeRng, betaSample, escalation, overridePrior, roughEstimate, choose, shortModel,
|
|
532
|
+
TYPE_ALIASES, resolveTaskType,
|
|
477
533
|
};
|