@kienntpixon/dieu-phoi 0.3.0 → 0.5.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 +33 -0
- package/README.md +10 -3
- package/agents/dp-exec.md +38 -0
- package/agents/dp-helper.md +23 -0
- package/agents/dp-scout.md +25 -0
- package/agents/dp-verify.md +25 -0
- package/bin/dieu-phoi.cjs +8 -3
- package/hook.cjs +600 -23
- package/lib/aggregate.cjs +98 -19
- package/lib/choose.cjs +79 -19
- package/lib/commands.cjs +206 -23
- package/lib/setup.cjs +71 -2
- package/package.json +2 -1
package/lib/aggregate.cjs
CHANGED
|
@@ -9,6 +9,15 @@ 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)(?:-|$)/;
|
|
18
|
+
// core 0.5.0: `aborted` with this reason = the agent stopped on the context watchdog and wrote handoff.md. Its
|
|
19
|
+
// continuation (next exec agent of the task with the same try) carries the attempt's agent-layer result.
|
|
20
|
+
const HANDOFF_REASON_RE = /^(?:auto-)?handoff(?:-|$)/;
|
|
12
21
|
|
|
13
22
|
function tsOf(ev) {
|
|
14
23
|
const t = Date.parse(ev && ev.ts);
|
|
@@ -56,7 +65,33 @@ function normalizeVerdict(ev) {
|
|
|
56
65
|
const by = ev.by === 'user' ? 'user' : 'agent';
|
|
57
66
|
let cause = ev.cause || null;
|
|
58
67
|
if (by === 'user' && ev.outcome === 'reject' && cause !== 'scope') cause = 'quality';
|
|
59
|
-
|
|
68
|
+
let outcome = ev.outcome;
|
|
69
|
+
if (by === 'agent' && outcome === 'timeout' && typeof ev.reason_code === 'string' && QUOTA_REASON_RE.test(ev.reason_code)) {
|
|
70
|
+
outcome = ABORTED;
|
|
71
|
+
}
|
|
72
|
+
return { by, outcome, cause, round: Number(ev.round) || null };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Task ids withdrawn with `dieu-phoi discard --task` (kind=discard). Every event of such a task — and every
|
|
77
|
+
* event of an agent dispatched for it — is left out of tasks, cells and steers (CONTRACT "Sự kiện cục bộ").
|
|
78
|
+
*/
|
|
79
|
+
function discardedTaskIds(events) {
|
|
80
|
+
const out = new Set();
|
|
81
|
+
for (const ev of events) if (ev && ev.kind === 'discard' && typeof ev.task_id === 'string' && ev.task_id) out.add(ev.task_id);
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Events with discarded tasks (and the discard records themselves) replaced by null; order kept. */
|
|
86
|
+
function withoutDiscarded(events) {
|
|
87
|
+
const gone = discardedTaskIds(events);
|
|
88
|
+
const agents = new Set(events.filter((ev) => ev && ev.kind === 'dispatch' && gone.has(ev.task_id)).map((ev) => ev.agent_id));
|
|
89
|
+
return events.map((ev) => {
|
|
90
|
+
if (!ev || typeof ev !== 'object' || ev.kind === 'discard') return null;
|
|
91
|
+
if (gone.has(ev.task_id) || agents.has(ev.agent_id)) return null;
|
|
92
|
+
if (typeof ev.agent_id === 'string' && ev.agent_id.startsWith('task:') && gone.has(ev.agent_id.slice(5))) return null;
|
|
93
|
+
return ev;
|
|
94
|
+
});
|
|
60
95
|
}
|
|
61
96
|
|
|
62
97
|
function blank(v) {
|
|
@@ -111,7 +146,9 @@ function buildAgents(events) {
|
|
|
111
146
|
const d = a.dispatch || {};
|
|
112
147
|
const c0 = a.completes[0] || {};
|
|
113
148
|
const cl = a.completes[a.completes.length - 1] || {};
|
|
114
|
-
const
|
|
149
|
+
const vAll = a.verdicts.slice().sort((x, y) => tsOf(x) - tsOf(y) || x._idx - y._idx);
|
|
150
|
+
// `aborted` (quota pause) is neither pass nor fail: it never sets the outcome nor counts as a result.
|
|
151
|
+
const v = vAll.filter((x) => x.outcome !== ABORTED);
|
|
115
152
|
const verdict = v.length ? v[v.length - 1] : null;
|
|
116
153
|
const firstVerdictTs = v.length ? tsOf(v[0]) : Infinity;
|
|
117
154
|
const pick = (k) => (d[k] !== undefined && d[k] !== null ? d[k] : (c0[k] !== undefined && c0[k] !== null ? c0[k] : null));
|
|
@@ -137,6 +174,10 @@ function buildAgents(events) {
|
|
|
137
174
|
tool_uses: a.completes.length ? sumOrNull(a.completes.map((x) => x.tool_uses)) : null,
|
|
138
175
|
outcome: verdict ? verdict.outcome : null,
|
|
139
176
|
first_outcome: v.length ? v[0].outcome : null,
|
|
177
|
+
// every non-aborted agent verdict in time order: a retry by SendMessage to the same agent is a new result
|
|
178
|
+
verdict_seq: v.map((x) => ({ outcome: x.outcome, ts: tsOf(x), idx: x._idx })),
|
|
179
|
+
aborted: vAll.length > 0 && vAll[vAll.length - 1].outcome === ABORTED,
|
|
180
|
+
handoff: vAll.some((x) => x.outcome === ABORTED && HANDOFF_REASON_RE.test(String(x.reason_code || ''))),
|
|
140
181
|
verify: verdict ? verdict.verify : null,
|
|
141
182
|
reason_code: verdict ? verdict.reason_code : null,
|
|
142
183
|
steers: a.steers.map((s) => s.steer_kind).filter(Boolean),
|
|
@@ -169,22 +210,41 @@ function buildTasks(agents, userVerdicts) {
|
|
|
169
210
|
const tasks = [];
|
|
170
211
|
for (const t of map.values()) {
|
|
171
212
|
const byTry = (x, y) => (x.try - y.try) || (x.ts - y.ts) || (x._idx - y._idx);
|
|
172
|
-
|
|
213
|
+
// gen-2 helpers (role=helper, own task_id) are never attempts
|
|
214
|
+
const attempts = t.agents.filter((a) => a.role !== 'verify' && a.role !== 'helper').sort(byTry);
|
|
173
215
|
const verifies = t.agents.filter((a) => a.role === 'verify');
|
|
174
216
|
// First exec attempt: lowest try among role=exec (ties: earliest). No exec attempt -> no sample.
|
|
175
|
-
const
|
|
217
|
+
const execs = t.agents.filter((a) => a.role === 'exec').sort(byTry);
|
|
218
|
+
const first = execs[0] || null;
|
|
219
|
+
// core 0.5.0: a first exec that stopped on a context handoff (no result of its own) passes the agent layer to its
|
|
220
|
+
// continuation(s) with the same try; the tier stays the first exec's.
|
|
221
|
+
let firstRes = first;
|
|
222
|
+
const chain = first ? [first] : [];
|
|
223
|
+
for (let i = 1; firstRes && !firstRes.first_outcome && firstRes.handoff && i < execs.length && execs[i].try === first.try; i++) {
|
|
224
|
+
firstRes = execs[i];
|
|
225
|
+
chain.push(firstRes);
|
|
226
|
+
}
|
|
176
227
|
const user = t.user.slice().sort((x, y) => ((x.round || 0) - (y.round || 0)) || (tsOf(x) - tsOf(y)) || (x._idx - y._idx));
|
|
177
228
|
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
229
|
+
// Results for escalation. Each agent verdict of an attempt is one result at its tier (a retry by
|
|
230
|
+
// SendMessage to the same agent is a new result); a user verdict adds a result to the latest attempt
|
|
231
|
+
// dispatched before it (reject/quality = fail, accept = pass). eff_outcome = the attempt's latest result.
|
|
232
|
+
for (const a of attempts) {
|
|
233
|
+
a.results = (a.verdict_seq || [])
|
|
234
|
+
.filter((x) => x.outcome === 'pass' || FAIL_OUTCOMES.has(x.outcome))
|
|
235
|
+
.map((x) => ({ outcome: x.outcome === 'pass' ? 'pass' : 'fail', ts: x.ts, idx: x.idx }));
|
|
236
|
+
a.eff_outcome = a.results.length ? a.results[a.results.length - 1].outcome : null;
|
|
237
|
+
}
|
|
181
238
|
for (const v of user) {
|
|
182
239
|
if (v.cause === 'scope') continue;
|
|
183
240
|
const vt = tsOf(v);
|
|
184
241
|
let target = null;
|
|
185
242
|
for (const a of attempts) if (a.ts <= vt && (!target || a.ts >= target.ts)) target = a;
|
|
186
243
|
if (!target) target = attempts[attempts.length - 1] || null;
|
|
187
|
-
if (target)
|
|
244
|
+
if (target) {
|
|
245
|
+
target.eff_outcome = v.outcome === 'accept' ? 'pass' : 'fail';
|
|
246
|
+
target.results.push({ outcome: target.eff_outcome, ts: vt, idx: v._idx, user: true });
|
|
247
|
+
}
|
|
188
248
|
}
|
|
189
249
|
|
|
190
250
|
const firstUser = user[0] || null;
|
|
@@ -194,8 +254,8 @@ function buildTasks(agents, userVerdicts) {
|
|
|
194
254
|
else userOutcome = firstUser.cause === 'scope' ? 'reject_scope' : 'reject_quality';
|
|
195
255
|
}
|
|
196
256
|
// Agent layer of the first exec attempt: its first agent verdict.
|
|
197
|
-
const agentFirst =
|
|
198
|
-
?
|
|
257
|
+
const agentFirst = firstRes && firstRes.first_outcome && (firstRes.first_outcome === 'pass' || FAIL_OUTCOMES.has(firstRes.first_outcome))
|
|
258
|
+
? firstRes.first_outcome === 'pass' : null;
|
|
199
259
|
let success = null;
|
|
200
260
|
let source = null;
|
|
201
261
|
if (userOutcome === 'accept') { success = true; source = 'user'; } else if (userOutcome === 'reject_quality') { success = false; source = 'user'; } else if (!userOutcome && agentFirst !== null) {
|
|
@@ -209,6 +269,11 @@ function buildTasks(agents, userVerdicts) {
|
|
|
209
269
|
task_id: t.task_id,
|
|
210
270
|
task_type: first ? first.task_type : (t.agents[0] && t.agents[0].task_type) || 'untagged',
|
|
211
271
|
first,
|
|
272
|
+
// the first attempt as a whole: its exec segment plus every handoff continuation (time/cost summed over all
|
|
273
|
+
// segments; the result comes from the last one, firstRes)
|
|
274
|
+
first_chain: chain,
|
|
275
|
+
first_cost: chain.length ? sumOrNull(chain.map((a) => a.cost_usd)) : null,
|
|
276
|
+
first_time_s: chain.length ? sumOrNull(chain.map((a) => a.duration_s)) : null,
|
|
212
277
|
attempts,
|
|
213
278
|
verifies,
|
|
214
279
|
user_verdicts: user,
|
|
@@ -258,8 +323,9 @@ function buildCells(tasks, opts) {
|
|
|
258
323
|
const passes = c.tasks.filter((t) => t.success).length;
|
|
259
324
|
const costs = c.tasks.map((t) => t.total_cost);
|
|
260
325
|
const timesMin = c.tasks.map((t) => (t.total_time_s === null ? null : t.total_time_s / 60));
|
|
261
|
-
|
|
262
|
-
const
|
|
326
|
+
// a handoff chain is one attempt: all its segments count (t.first alone would bill only the first segment)
|
|
327
|
+
const attemptTimes = c.tasks.map((t) => (t.first_time_s === null || t.first_time_s === undefined ? null : t.first_time_s / 60));
|
|
328
|
+
const attemptCosts = c.tasks.map((t) => (t.first_cost === undefined ? null : t.first_cost));
|
|
263
329
|
const costMean = mean(costs);
|
|
264
330
|
const timeMean = mean(timesMin);
|
|
265
331
|
out.push({
|
|
@@ -307,13 +373,19 @@ function canonicalCells(cells) {
|
|
|
307
373
|
return cells.map((c) => Object.fromEntries(CANONICAL_CELL_FIELDS.map((k) => [k, c[k] === undefined ? null : c[k]])));
|
|
308
374
|
}
|
|
309
375
|
|
|
310
|
-
/**
|
|
376
|
+
/**
|
|
377
|
+
* Steering per task_type, counted in distinct user actions (steer_id), not events. Core 0.4.0: only
|
|
378
|
+
* exec-phase steers count — a steer on a role=exec agent, or a task-level steer on a task that has an exec
|
|
379
|
+
* attempt. Steers on scouts/planners (gate-1 Q&A) and on verifiers go to `plan_phase_actions` and never
|
|
380
|
+
* touch actions / steer_rate.
|
|
381
|
+
*/
|
|
311
382
|
function buildSteers(tasks, steers, agents) {
|
|
312
383
|
const typeOfTask = new Map(tasks.map((t) => [t.task_id, t.task_type]));
|
|
384
|
+
const execTask = new Set(tasks.filter((t) => t.first && t.task_id).map((t) => t.task_id));
|
|
313
385
|
const agentById = new Map(agents.map((a) => [a.agent_id, a]));
|
|
314
386
|
const by = {};
|
|
315
387
|
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(),
|
|
388
|
+
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
389
|
});
|
|
318
390
|
for (const t of tasks) if (t.first) row(t.task_type).tasks++;
|
|
319
391
|
for (const s of steers) {
|
|
@@ -322,6 +394,8 @@ function buildSteers(tasks, steers, agents) {
|
|
|
322
394
|
const type = (taskId && typeOfTask.get(taskId)) || s.task_type || (a && a.task_type) || 'untagged';
|
|
323
395
|
const r = row(type);
|
|
324
396
|
const id = steerIdOf(s);
|
|
397
|
+
const execPhase = a ? a.role === 'exec' : !(taskId && typeOfTask.has(taskId) && !execTask.has(taskId));
|
|
398
|
+
if (!execPhase) { r._plan.add(id); continue; }
|
|
325
399
|
if (r._ids.has(id)) continue;
|
|
326
400
|
r._ids.add(id);
|
|
327
401
|
r.actions++;
|
|
@@ -334,6 +408,7 @@ function buildSteers(tasks, steers, agents) {
|
|
|
334
408
|
task_type: r.task_type, tasks: r.tasks, steered_tasks: steered, actions: r.actions,
|
|
335
409
|
minor: r.minor, plan: r.plan, redirect: r.redirect,
|
|
336
410
|
steer_rate: r.tasks ? round6(Math.min(1, steered / r.tasks)) : 0,
|
|
411
|
+
plan_phase_actions: Array.from(r._plan).filter((x) => !r._ids.has(x)).length,
|
|
337
412
|
};
|
|
338
413
|
return outRow;
|
|
339
414
|
}).sort((a, b) => b.steer_rate - a.steer_rate || a.task_type.localeCompare(b.task_type));
|
|
@@ -345,7 +420,8 @@ function finiteNum(v) {
|
|
|
345
420
|
|
|
346
421
|
/**
|
|
347
422
|
* 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
|
|
423
|
+
* dispatch events. Per session: ctx = max orch_ctx_tokens, usd = max orch_cum_usd minus orch_cum_usd of the
|
|
424
|
+
* session's first dispatch carrying a number (core 0.4.0; never negative), growth = max of
|
|
349
425
|
* max(0, orch_ctx_tokens - orch_ctx_baseline) over dispatches carrying both, comp = max orch_compactions
|
|
350
426
|
* (numbers only). compactions_total = sum of per-session comp, compactions_max = max.
|
|
351
427
|
* Each agent belongs to the session of its FIRST dispatch (file order); sub_usd_total sums the
|
|
@@ -357,11 +433,14 @@ function orchestrationSummary(events) {
|
|
|
357
433
|
for (const ev of events) {
|
|
358
434
|
if (!ev || ev.kind !== 'dispatch' || typeof ev.session_id !== 'string' || !ev.session_id) continue;
|
|
359
435
|
const sid = ev.session_id;
|
|
360
|
-
if (!sessions.has(sid)) sessions.set(sid, { ctx: null, usd: null, growth: null, comp: null });
|
|
436
|
+
if (!sessions.has(sid)) sessions.set(sid, { ctx: null, usd: null, base: null, growth: null, comp: null });
|
|
361
437
|
const s = sessions.get(sid);
|
|
362
438
|
const c = finiteNum(ev.orch_ctx_tokens);
|
|
363
439
|
const u = finiteNum(ev.orch_cum_usd);
|
|
364
440
|
if (c !== null && (s.ctx === null || c > s.ctx)) s.ctx = c;
|
|
441
|
+
// core 0.4.0: orchestrator cost counts from the session's FIRST dispatch carrying a number (file order),
|
|
442
|
+
// not from session start — whatever the session did before it started dispatching is not orchestration.
|
|
443
|
+
if (u !== null && s.base === null) s.base = u;
|
|
365
444
|
if (u !== null && (s.usd === null || u > s.usd)) s.usd = u;
|
|
366
445
|
const k = finiteNum(ev.orch_compactions);
|
|
367
446
|
if (k !== null && (s.comp === null || k > s.comp)) s.comp = k;
|
|
@@ -386,7 +465,7 @@ function orchestrationSummary(events) {
|
|
|
386
465
|
if (s.ctx !== null) ctxs.push(s.ctx);
|
|
387
466
|
if (s.growth !== null) growths.push(s.growth);
|
|
388
467
|
if (s.comp !== null) comps.push(s.comp);
|
|
389
|
-
if (s.usd !== null) orch += s.usd;
|
|
468
|
+
if (s.usd !== null) orch += Math.max(0, s.usd - s.base);
|
|
390
469
|
}
|
|
391
470
|
return {
|
|
392
471
|
sessions: sessions.size,
|
|
@@ -403,7 +482,7 @@ function orchestrationSummary(events) {
|
|
|
403
482
|
}
|
|
404
483
|
|
|
405
484
|
function analyze(events, opts) {
|
|
406
|
-
const { agents, userVerdicts, overrides, steers } = buildAgents(events);
|
|
485
|
+
const { agents, userVerdicts, overrides, steers } = buildAgents(withoutDiscarded(events));
|
|
407
486
|
const tasks = buildTasks(agents, userVerdicts);
|
|
408
487
|
const cells = buildCells(tasks, opts);
|
|
409
488
|
return {
|
|
@@ -413,6 +492,6 @@ function analyze(events, opts) {
|
|
|
413
492
|
}
|
|
414
493
|
|
|
415
494
|
module.exports = {
|
|
416
|
-
MIN_N, FAIL_OUTCOMES, HEAVY_STEERS, CANONICAL_CELL_FIELDS, normalizeVerdict, isInternalComplete, buildAgents, buildTasks,
|
|
495
|
+
MIN_N, FAIL_OUTCOMES, HEAVY_STEERS, ABORTED, QUOTA_REASON_RE, HANDOFF_REASON_RE, discardedTaskIds, withoutDiscarded, CANONICAL_CELL_FIELDS, normalizeVerdict, isInternalComplete, buildAgents, buildTasks,
|
|
417
496
|
buildCells, buildSteers, canonicalCells, orchestrationSummary, analyze, cellKey, mean, percentile, tsOf, steerIdOf,
|
|
418
497
|
};
|
package/lib/choose.cjs
CHANGED
|
@@ -17,9 +17,11 @@ const LADDER = [
|
|
|
17
17
|
{ model: 'opus', effort: 'medium' },
|
|
18
18
|
];
|
|
19
19
|
const TOP = LADDER.length - 1;
|
|
20
|
+
const HELPER_TOP = 4; // sonnet/high: ceiling for role=helper (gen-2)
|
|
20
21
|
const EFFORT_RANK = { low: 0, medium: 1, high: 2, xhigh: 3, max: 4 };
|
|
21
22
|
|
|
22
|
-
// Built-in defaults = skill/profile/profile.json (
|
|
23
|
+
// Built-in defaults = skill/profile/profile.json for the fixed task types (time caps as tuned 2026-10-12: feature-impl 80,
|
|
24
|
+
// content-write 40, debug 55 — the old ones sat below the observed p50); used only when no profile is found.
|
|
23
25
|
const DEFAULT_PRIORS = {
|
|
24
26
|
search: 'haiku/medium',
|
|
25
27
|
extract: 'haiku/medium',
|
|
@@ -38,13 +40,13 @@ const DEFAULT_TIME_CAP_MIN = {
|
|
|
38
40
|
search: 3,
|
|
39
41
|
extract: 5,
|
|
40
42
|
'mechanical-edit': 10,
|
|
41
|
-
'feature-impl':
|
|
42
|
-
debug:
|
|
43
|
+
'feature-impl': 80,
|
|
44
|
+
debug: 55,
|
|
43
45
|
refactor: 30,
|
|
44
46
|
'test-write': 15,
|
|
45
47
|
review: 10,
|
|
46
48
|
'research-synth': 20,
|
|
47
|
-
'content-write':
|
|
49
|
+
'content-write': 40,
|
|
48
50
|
'plan-design': 15,
|
|
49
51
|
'data-analysis': 15,
|
|
50
52
|
};
|
|
@@ -187,25 +189,35 @@ const OPUS_MEDIUM = 5;
|
|
|
187
189
|
* - one fail at a tier -> same tier again (retry with the error);
|
|
188
190
|
* - two fails at the same tier -> next tier up;
|
|
189
191
|
* - 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
|
-
*
|
|
192
|
+
* Fail = agent fail|timeout or user reject cause=quality. Core 0.4.0: every result counts, so a retry by
|
|
193
|
+
* SendMessage to the same agent (fail, then fail again) is two fails at that tier; `aborted` (quota pause)
|
|
194
|
+
* is never a result. Attempts steered plan/redirect by the user are ignored.
|
|
192
195
|
*/
|
|
193
196
|
function escalation(task, role) {
|
|
194
197
|
const none = { floor: 0, ask: false, note: null, fails: 0, tried: [] };
|
|
195
198
|
if (!task) return none;
|
|
196
199
|
const att = task.attempts
|
|
197
|
-
.filter((a) => a.
|
|
198
|
-
.map((a) => ({
|
|
199
|
-
|
|
200
|
+
.filter((a) => a.role !== 'verify' && !a.steered)
|
|
201
|
+
.map((a) => ({
|
|
202
|
+
a,
|
|
203
|
+
i: tierIndex(a.model, a.effort),
|
|
204
|
+
results: Array.isArray(a.results) ? a.results : (a.eff_outcome ? [{ outcome: a.eff_outcome, ts: a.ts, idx: 0 }] : []),
|
|
205
|
+
}))
|
|
206
|
+
.filter((x) => x.i !== null && x.results.length)
|
|
200
207
|
.sort((x, y) => (x.a.try - y.a.try) || (x.a.ts - y.a.ts));
|
|
201
208
|
if (!att.length) return none;
|
|
209
|
+
const res = [];
|
|
210
|
+
for (const x of att) {
|
|
211
|
+
const rs = x.results.slice().sort((p, q) => (p.ts - q.ts) || ((p.idx || 0) - (q.idx || 0)));
|
|
212
|
+
for (const r of rs) res.push({ i: x.i, outcome: r.outcome });
|
|
213
|
+
}
|
|
202
214
|
const tried = Array.from(new Set(att.map((x) => tierName(x.i))));
|
|
203
|
-
const totalFails =
|
|
204
|
-
const last =
|
|
215
|
+
const totalFails = res.filter((r) => r.outcome === 'fail').length;
|
|
216
|
+
const last = res[res.length - 1];
|
|
205
217
|
const cur = last.i;
|
|
206
218
|
const base = { tried, fails: totalFails, ask: false };
|
|
207
|
-
if (last.
|
|
208
|
-
const fails =
|
|
219
|
+
if (last.outcome !== 'fail') return Object.assign(base, { floor: cur, note: null });
|
|
220
|
+
const fails = res.filter((r) => r.i === cur && r.outcome === 'fail').length;
|
|
209
221
|
if (fails < 2) {
|
|
210
222
|
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
223
|
}
|
|
@@ -287,6 +299,40 @@ function estimateFor(idx, cell, pricing) {
|
|
|
287
299
|
return roughEstimate(idx, pricing);
|
|
288
300
|
}
|
|
289
301
|
|
|
302
|
+
// ------------------------------------------------------------------ task type guard
|
|
303
|
+
|
|
304
|
+
// Ad-hoc labels seen in real logs → the closest fixed type (core 0.4.0). A label maps only when its target
|
|
305
|
+
// is known (fixed list or the profile's extra_task_types), e.g. art-fix → art-gen needs art-gen in the profile.
|
|
306
|
+
const TYPE_ALIASES = {
|
|
307
|
+
'bug-fix': 'debug', bugfix: 'debug', fix: 'debug', bug: 'debug',
|
|
308
|
+
config: 'mechanical-edit', shell: 'mechanical-edit', deploy: 'mechanical-edit', ops: 'mechanical-edit', edit: 'mechanical-edit',
|
|
309
|
+
docs: 'content-write', doc: 'content-write', documentation: 'content-write', writing: 'content-write', copy: 'content-write',
|
|
310
|
+
design: 'plan-design', plan: 'plan-design', planning: 'plan-design', architecture: 'plan-design',
|
|
311
|
+
research: 'research-synth', synth: 'research-synth',
|
|
312
|
+
test: 'test-write', tests: 'test-write',
|
|
313
|
+
analysis: 'data-analysis', data: 'data-analysis',
|
|
314
|
+
impl: 'feature-impl', feature: 'feature-impl', implement: 'feature-impl',
|
|
315
|
+
'code-review': 'review',
|
|
316
|
+
'art-fix': 'art-gen', art: 'art-gen',
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* { type, known, mapped_from, warning }: a fixed/extra type passes; an ad-hoc label with a known alias target
|
|
321
|
+
* maps to it (with a warning); anything else stays unknown (choose asks the user to pick a fixed type).
|
|
322
|
+
*/
|
|
323
|
+
function resolveTaskType(raw, profile) {
|
|
324
|
+
const t = String(raw || '').toLowerCase();
|
|
325
|
+
const extra = (profile && profile.extra_task_types) || [];
|
|
326
|
+
const isKnown = (x) => core.TASK_TYPES.includes(x) || extra.includes(x);
|
|
327
|
+
if (isKnown(t)) return { type: t, known: true, mapped_from: null, warning: null };
|
|
328
|
+
const target = TYPE_ALIASES[t];
|
|
329
|
+
if (target && isKnown(target)) {
|
|
330
|
+
return { type: target, known: true, mapped_from: t, warning: `loại "${t}" không cố định → dùng "${target}" (ghi --type ${target} trong thẻ)` };
|
|
331
|
+
}
|
|
332
|
+
const list = core.TASK_TYPES.concat(extra).join(', ');
|
|
333
|
+
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` };
|
|
334
|
+
}
|
|
335
|
+
|
|
290
336
|
// ------------------------------------------------------------------ choose
|
|
291
337
|
|
|
292
338
|
/**
|
|
@@ -296,8 +342,8 @@ function estimateFor(idx, cell, pricing) {
|
|
|
296
342
|
* Throws when an override goes below a safety floor or off the ladder.
|
|
297
343
|
*/
|
|
298
344
|
function choose(opts) {
|
|
299
|
-
const
|
|
300
|
-
if (!
|
|
345
|
+
const rawType = String(opts.type || '').toLowerCase();
|
|
346
|
+
if (!rawType) throw new Error('Thiếu --type');
|
|
301
347
|
const score = Number(opts.score);
|
|
302
348
|
if (opts.score === undefined || opts.score === null || opts.score === true || !isFinite(score)
|
|
303
349
|
|| score < 0 || score > 12) throw new Error('--score phải trong 0–12');
|
|
@@ -327,12 +373,15 @@ function choose(opts) {
|
|
|
327
373
|
const notes = [];
|
|
328
374
|
|
|
329
375
|
const analysis = agg.analyze(opts.events || [], { lambda });
|
|
330
|
-
const
|
|
376
|
+
const rt = resolveTaskType(rawType, profile);
|
|
377
|
+
const type = rt.type;
|
|
378
|
+
const known = rt.known;
|
|
379
|
+
if (rt.warning) notes.push(rt.warning);
|
|
331
380
|
let prior = profile.priors[type];
|
|
332
381
|
let priorWhy = 'prior';
|
|
333
382
|
if (prior === undefined || prior === null) {
|
|
334
383
|
prior = OPUS_MEDIUM;
|
|
335
|
-
notes.push(
|
|
384
|
+
if (known) notes.push('không có prior, dùng opus/medium');
|
|
336
385
|
}
|
|
337
386
|
const ov = overridePrior(analysis.overrides, type);
|
|
338
387
|
if (ov && ov.tier !== prior) {
|
|
@@ -370,7 +419,10 @@ function choose(opts) {
|
|
|
370
419
|
&& shortModel(c.model) === LADDER[i].model && c.effort === LADDER[i].effort) || null;
|
|
371
420
|
|
|
372
421
|
const src = profile.source || 'builtin';
|
|
373
|
-
const finish = (
|
|
422
|
+
const finish = (idx0, reason0, explored, extra) => {
|
|
423
|
+
// core 0.5.0: a gen-2 helper (spawned by a dp-exec) is haiku|sonnet only — clamp to sonnet/high
|
|
424
|
+
const idx = role === 'helper' ? Math.min(idx0, HELPER_TOP) : idx0;
|
|
425
|
+
const reason = idx === idx0 ? reason0 : `${reason0}; helper: trần ${tierName(HELPER_TOP)}`;
|
|
374
426
|
const t = LADDER[idx];
|
|
375
427
|
const cell = cellAt(idx);
|
|
376
428
|
return Object.assign({
|
|
@@ -382,10 +434,17 @@ function choose(opts) {
|
|
|
382
434
|
explored,
|
|
383
435
|
tier: tierName(idx),
|
|
384
436
|
role,
|
|
437
|
+
task_type: type,
|
|
385
438
|
budget_s: budgetFor(cell),
|
|
386
|
-
}, estimateFor(idx, cell, opts.pricing), { profile_source: src },
|
|
439
|
+
}, estimateFor(idx, cell, opts.pricing), { profile_source: src },
|
|
440
|
+
rt.mapped_from ? { mapped_from: rt.mapped_from } : {}, rt.warning ? { type_warning: rt.warning } : {}, extra || {});
|
|
387
441
|
};
|
|
388
442
|
|
|
443
|
+
// Unknown task type (not fixed, not in the profile, no alias): its stats would never pool — ask first.
|
|
444
|
+
if (!known) {
|
|
445
|
+
return finish(Math.max(prior, safetyFloor), rt.warning, false, { action: 'ask_user', unknown_type: true });
|
|
446
|
+
}
|
|
447
|
+
|
|
389
448
|
if (overrideIdx !== null && overrideIdx < safetyFloor) {
|
|
390
449
|
throw new Error(`Từ chối override ${tierName(overrideIdx)}: dưới sàn an toàn ${tierName(safetyFloor)} (${floorWhy.join(', ')}).`);
|
|
391
450
|
}
|
|
@@ -474,4 +533,5 @@ module.exports = {
|
|
|
474
533
|
DEFAULT_ORCH_CTX_SOFT_CAP,
|
|
475
534
|
LADDER, DEFAULT_PRIORS, DEFAULT_LAMBDA, DEFAULT_TIME_CAP_MIN, ROUGH, tierIndex, tierName, parsePrior,
|
|
476
535
|
parseTier, loadProfile, makeRng, betaSample, escalation, overridePrior, roughEstimate, choose, shortModel,
|
|
536
|
+
TYPE_ALIASES, resolveTaskType,
|
|
477
537
|
};
|