@piagent/platform 1.46.0 → 1.46.1
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 +20 -0
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/distribution-standard.md +6 -6
- package/docs/en/architecture.md +1 -1
- package/docs/harness-conformance.md +22 -5
- package/docs/languages.json +2 -2
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +1 -0
- package/docs/release-install-policy.md +2 -2
- package/docs/team-onboarding.md +2 -2
- package/docs/vi/architecture.md +1 -1
- package/docs-site/architecture.html +1 -1
- package/docs-site/assets/search-en.json +1 -1
- package/docs-site/assets/search-vi.json +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/context-engine.html +1 -1
- package/docs-site/ecosystem.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/ecosystem.html +1 -1
- package/docs-site/en/index.html +9 -9
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +1 -1
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/update.html +3 -3
- package/docs-site/en/whats-new.html +27 -13
- package/docs-site/en/windows.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +9 -9
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/releases.json +44 -0
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/update.html +3 -3
- package/docs-site/whats-new.html +27 -13
- package/docs-site/windows.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
- package/package.json +1 -1
- package/packages/piagent-core/managed/copies.mjs +61 -0
- package/packages/piagent-core/managed/grant-share.mjs +9 -1
- package/packages/piagent-core/managed/helper-roles.mjs +5 -3
- package/packages/piagent-core/managed/helper-run.mjs +120 -52
- package/packages/piagent-core/managed/helper-settlement.mjs +5 -1
- package/packages/piagent-core/managed/ledger.mjs +187 -0
- package/packages/piagent-core/managed/objections.mjs +49 -27
- package/packages/piagent-core/managed/patch-snapshot.mjs +9 -0
- package/packages/piagent-core/managed/session.mjs +14 -14
- package/packages/piagent-core/managed/toolchain.mjs +7 -3
- package/packages/piagent-core/managed/workflow.mjs +32 -26
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/managed-failure.mjs +8 -0
- package/scripts/company-terminal.mjs +19 -1
package/docs-site/whats-new.html
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
|
|
38
38
|
<span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
|
|
39
39
|
</a>
|
|
40
|
-
<a class="version-badge" href="/whats-new" data-docs-version="v1.46.
|
|
40
|
+
<a class="version-badge" href="/whats-new" data-docs-version="v1.46.1" title="v1.46.1 docs · VI"><span class="dot"></span>v1.46.1</a>
|
|
41
41
|
<nav class="top-nav" aria-label="Tài liệu">
|
|
42
42
|
<a href="/quickstart">Tài liệu</a>
|
|
43
43
|
<a href="/whats-new" aria-current="page">Changelog</a>
|
|
@@ -108,7 +108,22 @@
|
|
|
108
108
|
<div class="release-filter" role="toolbar" aria-label="Lọc theo loại thay đổi"><button type="button" data-filter-kind="all" aria-pressed="true">Tất cả</button><button type="button" data-filter-kind="new" aria-pressed="false">Mới</button><button type="button" data-filter-kind="improved" aria-pressed="false">Cải thiện</button><button type="button" data-filter-kind="fixed" aria-pressed="false">Sửa lỗi</button><button type="button" data-filter-kind="security" aria-pressed="false">Bảo mật</button></div>
|
|
109
109
|
<section class="section releases" data-releases>
|
|
110
110
|
<h2 id="latest" class="sr-only">Mới nhất</h2>
|
|
111
|
-
<article class="release latest" id="v1-46-
|
|
111
|
+
<article class="release latest" id="v1-46-1" data-release>
|
|
112
|
+
<div class="release-meta"><a class="release-version" href="#v1-46-1">v1.46.1</a><time class="release-date" datetime="2026-10-28">28/10/2026</time></div>
|
|
113
|
+
<div class="release-body">
|
|
114
|
+
<h3 data-toc-skip id="v1-46-1-title">Phản biện có sổ ghi, review một nhịp, subagent song song khi được yêu cầu</h3>
|
|
115
|
+
<p>Harness công ty ghi một sổ phản biện cùng quyết định của main, review một nhịp mỗi lượt, và chỉ chạy 2–3 helper cùng role song song khi member yêu cầu.</p>
|
|
116
|
+
<ul class="release-items">
|
|
117
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Phản biện brief, finding review và claim fail chung một sổ; main ghi quyết định bằng resolve_objection, phản biện sai tiền đề chặn sửa file đến khi được quyết.</span></li>
|
|
118
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Mặc định mỗi role một helper; khi member yêu cầu 2–3 helper cùng role, chúng chạy song song, mỗi helper một grant với Agent Studio 0.50.0 và Agent Watch 0.15.17.</span></li>
|
|
119
|
+
<li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Review một nhịp mỗi lượt: review bắt buộc do harness làm, review lại sau khi sửa chỉ đọc file đã đổi, finding minor chỉ liệt kê.</span></li>
|
|
120
|
+
<li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Terminal công ty hiện checklist và việc từng subagent đang làm ngay trên ô nhập.</span></li>
|
|
121
|
+
<li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>piagent studio cài fd ở lần mở đầu tiên nên gợi ý @ hiện file và thư mục; trước đây fd không bao giờ được tải, kể cả khi đã có ở /opt/homebrew/bin.</span></li>
|
|
122
|
+
</ul>
|
|
123
|
+
<p class="release-requires"><strong>Cần:</strong> Agent Studio 0.50.0 và Agent Watch 0.15.17 (Windows 0.1.18) để subagent chạy song song</p>
|
|
124
|
+
</div>
|
|
125
|
+
</article>
|
|
126
|
+
<article class="release" id="v1-46-0" data-release>
|
|
112
127
|
<div class="release-meta"><a class="release-version" href="#v1-46-0">v1.46.0</a><time class="release-date" datetime="2026-10-28">28/10/2026</time></div>
|
|
113
128
|
<div class="release-body">
|
|
114
129
|
<h3 data-toc-skip id="v1-46-0-title">Piagent chạy trong Pi home riêng, pi của bạn giữ nguyên bản gốc</h3>
|
|
@@ -411,22 +426,21 @@
|
|
|
411
426
|
</ul>
|
|
412
427
|
</div>
|
|
413
428
|
</article>
|
|
414
|
-
<article class="release" id="v1-38-1" data-release>
|
|
415
|
-
<div class="release-meta"><a class="release-version" href="#v1-38-1">v1.38.1</a><time class="release-date" datetime="2026-10-23">23/10/2026</time></div>
|
|
416
|
-
<div class="release-body">
|
|
417
|
-
<h3 data-toc-skip id="v1-38-1-title">Bản phát hành chính thức của v1.38.0</h3>
|
|
418
|
-
<p>v1.38.0 không qua release check vì hai test nên chưa từng được phát hành; v1.38.1 là cùng bản đó với hai test đã được sửa.</p>
|
|
419
|
-
<ul class="release-items">
|
|
420
|
-
<li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Hai test phát hành nay pass trên Linux và máy chạy chậm, nên editor, terminal, review, checkpoint và Bypass của v1.38.0 có trong bản này.</span></li>
|
|
421
|
-
</ul>
|
|
422
|
-
</div>
|
|
423
|
-
</article>
|
|
424
429
|
</section>
|
|
425
430
|
<section class="section archive">
|
|
426
431
|
<h2 id="archive">Các bản trước</h2>
|
|
427
432
|
<p class="section-lead">Bản cũ hơn, gom theo tháng. Mở một tháng để xem từng thay đổi.</p>
|
|
428
433
|
<details class="release-archive">
|
|
429
|
-
<summary><span>Tháng 10/2026</span><span class="archive-count">
|
|
434
|
+
<summary><span>Tháng 10/2026</span><span class="archive-count">58</span></summary>
|
|
435
|
+
<div class="archive-row" id="v1-38-1">
|
|
436
|
+
<div class="archive-head"><a class="release-version" href="#v1-38-1">v1.38.1</a> <span class="release-date">23/10/2026</span></div>
|
|
437
|
+
<div class="archive-body">
|
|
438
|
+
<p><strong>Bản phát hành chính thức của v1.38.0</strong> — v1.38.0 không qua release check vì hai test nên chưa từng được phát hành; v1.38.1 là cùng bản đó với hai test đã được sửa.</p>
|
|
439
|
+
<ul class="release-items">
|
|
440
|
+
<li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Hai test phát hành nay pass trên Linux và máy chạy chậm, nên editor, terminal, review, checkpoint và Bypass của v1.38.0 có trong bản này.</span></li>
|
|
441
|
+
</ul>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
430
444
|
<div class="archive-row" id="v1-38-0">
|
|
431
445
|
<div class="archive-head"><a class="release-version" href="#v1-38-0">v1.38.0</a> <span class="release-date">23/10/2026</span></div>
|
|
432
446
|
<div class="archive-body">
|
package/docs-site/windows.html
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
|
|
38
38
|
<span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
|
|
39
39
|
</a>
|
|
40
|
-
<a class="version-badge" href="/whats-new" data-docs-version="v1.46.
|
|
40
|
+
<a class="version-badge" href="/whats-new" data-docs-version="v1.46.1" title="v1.46.1 docs · VI"><span class="dot"></span>v1.46.1</a>
|
|
41
41
|
<nav class="top-nav" aria-label="Tài liệu">
|
|
42
42
|
<a href="/quickstart">Tài liệu</a>
|
|
43
43
|
<a href="/whats-new">Changelog</a>
|
package/docs-site/workflows.html
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
|
|
38
38
|
<span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
|
|
39
39
|
</a>
|
|
40
|
-
<a class="version-badge" href="/whats-new" data-docs-version="v1.46.
|
|
40
|
+
<a class="version-badge" href="/whats-new" data-docs-version="v1.46.1" title="v1.46.1 docs · VI"><span class="dot"></span>v1.46.1</a>
|
|
41
41
|
<nav class="top-nav" aria-label="Tài liệu">
|
|
42
42
|
<a href="/quickstart">Tài liệu</a>
|
|
43
43
|
<a href="/whats-new">Changelog</a>
|
|
@@ -256,8 +256,8 @@
|
|
|
256
256
|
},
|
|
257
257
|
{
|
|
258
258
|
"path": "tests",
|
|
259
|
-
"fileCount":
|
|
260
|
-
"sha256": "
|
|
259
|
+
"fileCount": 636,
|
|
260
|
+
"sha256": "143847ccd4abfed97f9fc6bc5f7e5d654eb1cf83507ed473f5d2344327b14d8e"
|
|
261
261
|
}
|
|
262
262
|
],
|
|
263
263
|
"custodianComparisonRules": {
|
package/package.json
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { memberMessages, memberRequest } from './objections.mjs';
|
|
2
|
+
|
|
3
|
+
// Several helpers of one role, at the member's request only ("use 3
|
|
4
|
+
// reviewers", "cho 3 scout"): one delegate call runs 2 to MAX_COPIES helper
|
|
5
|
+
// sessions of the role side by side, each with its own context, and their
|
|
6
|
+
// answers merge into one. The main agent never adds helpers on its own, and
|
|
7
|
+
// the harness never does: the call quotes the member's words that ask for
|
|
8
|
+
// them, and those words must be the member's own and name the number. The
|
|
9
|
+
// copies share the role's grant, so Studio counts the role once while each
|
|
10
|
+
// copy counts against the role's calls for the message.
|
|
11
|
+
export const MAX_COPIES = 3;
|
|
12
|
+
const COUNTS = { two: 2, three: 3, hai: 2, ba: 3 };
|
|
13
|
+
// Words that say the number is of helpers, not of files or steps.
|
|
14
|
+
const HELPER_WORDS = /agent|helper|review|scout|research|verif|nghiên cứu|kiểm|xác minh|trinh sát/;
|
|
15
|
+
const normal = text => String(text ?? '').toLowerCase().normalize('NFC').replace(/\s+/g, ' ').trim();
|
|
16
|
+
// The largest helper count the words name (a digit, or two/three in English or Vietnamese).
|
|
17
|
+
export function namedCount(words) {
|
|
18
|
+
const found = [...normal(words).matchAll(/(?<![\p{L}\d])(\d+|two|three|hai|ba)(?![\p{L}\d])/gu)].map(m => COUNTS[m[1]] ?? Number(m[1]));
|
|
19
|
+
return found.length ? Math.max(...found) : 0;
|
|
20
|
+
}
|
|
21
|
+
// The copies a delegate call asks for, checked against the member's words;
|
|
22
|
+
// null for a plain call. `split`: one line per copy naming its share of the work.
|
|
23
|
+
export function fanOut(manager, role, args, harness) {
|
|
24
|
+
const split = Array.isArray(args?.split) ? args.split.map(s => typeof s === 'string' ? s.trim().slice(0, 500) : '').filter(Boolean) : [];
|
|
25
|
+
const n = split.length || (Number.isInteger(args?.copies) ? args.copies : 1);
|
|
26
|
+
if (n <= 1 && !args?.member_words) return null;
|
|
27
|
+
if (harness) throw Error('managed-copies-invalid: the harness runs one helper per call.');
|
|
28
|
+
if (n < 2 || n > MAX_COPIES) throw Error(`managed-copies-invalid: ask for 2 to ${MAX_COPIES} ${role} helpers (copies, or one split line per helper).`);
|
|
29
|
+
const words = normal(args?.member_words);
|
|
30
|
+
if (words.length < 3 || !memberRequest(memberMessages(manager)).some(text => normal(text).includes(words)) || namedCount(words) < n || !HELPER_WORDS.test(words))
|
|
31
|
+
throw Error(`managed-copies-not-asked: more than one ${role} helper runs only when the member asks for it. Quote in member_words the member's own words that ask for ${n} (verbatim, with the number); otherwise use one helper.`);
|
|
32
|
+
return { n, split: split.length ? split : null };
|
|
33
|
+
}
|
|
34
|
+
// What one copy reads under its brief.
|
|
35
|
+
export function copyNote(role, i, fan) {
|
|
36
|
+
return `\n\nThe member asked for ${fan.n} ${role} helpers; you are ${role} helper ${i + 1} of ${fan.n}. ${fan.split ? `Your share of the work: ${fan.split[i]}. The others cover the rest.` : 'The others have the same brief: answer on your own judgement.'}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const jsonOf = reply => {
|
|
40
|
+
for (const raw of [...String(reply).matchAll(/```json\s*([\s\S]*?)```/g)].map(m => m[1]).reverse()) { try { const v = JSON.parse(raw); if (v && typeof v === 'object') return v; } catch { /* next */ } }
|
|
41
|
+
return null;
|
|
42
|
+
};
|
|
43
|
+
const unique = (list, key) => { const seen = new Set(); return list.filter(x => { const k = key(x); if (seen.has(k)) return false; seen.add(k); return true; }); };
|
|
44
|
+
const SEVERITY = { blocking: 0, major: 1, minor: 2 };
|
|
45
|
+
// One answer from the copies: each helper's prose (its JSON left out), then
|
|
46
|
+
// one JSON block joining their findings, verdicts and objections, so the
|
|
47
|
+
// review, verify and objection readers see every copy. `parsed`: every copy
|
|
48
|
+
// gave its JSON. A finding or a failed claim any copy reports stands.
|
|
49
|
+
export function mergeCopies(role, label, answers, { findingsOf, issuesOf, verdictsOf }) {
|
|
50
|
+
const n = answers.length, room = Math.floor(12000 / n);
|
|
51
|
+
const findings = role === 'review' ? unique(answers.flatMap(a => findingsOf(a.reply) ?? []), f => `${f.severity}|${f.file}|${f.line}|${f.issue}`).sort((a, b) => SEVERITY[a.severity] - SEVERITY[b.severity]) : null;
|
|
52
|
+
const verdicts = role === 'verify' ? unique(answers.flatMap(a => verdictsOf(a.reply) ?? []), v => `${v.status}|${v.claim}`) : null;
|
|
53
|
+
const issues = unique(answers.flatMap(a => issuesOf(a.reply) ?? []), i => `${i.kind}|${i.detail}`).slice(0, 5);
|
|
54
|
+
const parsed = answers.every(a => jsonOf(a.reply));
|
|
55
|
+
const summaries = answers.map(a => jsonOf(a.reply)?.summary).filter(s => typeof s === 'string').map(s => s.slice(0, 200));
|
|
56
|
+
const json = { ...(findings ? { findings: findings.slice(0, 40) } : {}), ...(verdicts ? { verdicts: verdicts.slice(0, 40) } : {}), brief_issues: issues,
|
|
57
|
+
summary: `${n} ${role} helpers${summaries.length ? ': ' + summaries.join(' | ') : ''}`.slice(0, 900) };
|
|
58
|
+
const body = answers.map((a, i) => `### ${label} helper ${i + 1} of ${n}\n${String(a.reply).replace(/```json\s*[\s\S]*?```/g, '').trim().slice(0, room)}`).join('\n\n');
|
|
59
|
+
return { reply: `${body}\n\n\`\`\`json\n${JSON.stringify(json)}\n\`\`\``, usage: answers.reduce((total, a) => total + a.usage, 0),
|
|
60
|
+
...(findings ? { findings, parsed, blocking: findings.filter(f => f.severity === 'blocking').length } : {}) };
|
|
61
|
+
}
|
|
@@ -7,7 +7,7 @@ export function shareGrant(owner, role) {
|
|
|
7
7
|
owner.sharedGrants ??= new Map();
|
|
8
8
|
const shared = owner.sharedGrants.get(role);
|
|
9
9
|
if (shared && Date.now() - shared.at < 60_000) { shared.users++; return shared; }
|
|
10
|
-
const entry = { at: Date.now(), users: 1, grant: owner.broker.request('renew',
|
|
10
|
+
const entry = { at: Date.now(), users: 1, grant: owner.broker.request('renew', slotArgs(role)) };
|
|
11
11
|
entry.grant.catch(() => { if (owner.sharedGrants.get(role) === entry) owner.sharedGrants.delete(role); });
|
|
12
12
|
owner.sharedGrants.set(role, entry);
|
|
13
13
|
return entry;
|
|
@@ -16,3 +16,11 @@ export function shareGrant(owner, role) {
|
|
|
16
16
|
export function releaseGrant(owner, role, entry) {
|
|
17
17
|
if (--entry.users <= 0 && owner.sharedGrants?.get(role) === entry) owner.sharedGrants.delete(role);
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
// A helper the member asked for beside another of its role holds a grant of
|
|
21
|
+
// its own: its key is role#slot (slot 2 or 3), the broker's {role, slot}.
|
|
22
|
+
export const baseRole = key => String(key).split('#')[0];
|
|
23
|
+
export function slotArgs(key) {
|
|
24
|
+
const [role, slot] = String(key).split('#');
|
|
25
|
+
return slot ? { role, slot: Number(slot) } : { role };
|
|
26
|
+
}
|
|
@@ -44,7 +44,9 @@ export function helperPrompt(roles) {
|
|
|
44
44
|
: `Use the research helper (delegate role "research") for anything on the web (current information, news, documentation) and ${WIDE_READING}; work from its findings and do small code lookups yourself.`);
|
|
45
45
|
if (roles.includes('verify')) lines.push('Before you report a non-trivial result as done, you may have the verify helper (delegate role "verify") confirm it independently: it runs the repository checks on the current code and checks claims against the code and primary sources, answering pass, fail or unverifiable with evidence.');
|
|
46
46
|
// Helpers read the member's request beside the brief and may object to it.
|
|
47
|
-
if (lines.length) lines.push('A helper sees the member\'s request next to your brief and may object to the brief with evidence (brief_issues). You decide, but
|
|
47
|
+
if (lines.length) lines.push('A helper sees the member\'s request next to your brief and may object to the brief with evidence (brief_issues); each objection gets an id. You decide, but check each one at the place its evidence names and record your decision with resolve_objection: accepted with what you changed (a corrected brief, the plan, the code), or rejected with what you checked and why. An objection that the brief rests on a wrong premise or conflicts with the request refuses file changes until you decide it; a rejected one goes back to the helper, and a disagreement that remains goes to the member, as does a second objection from the same helper for one message.');
|
|
48
|
+
// More than one helper of a role, only at the member's word (copies.mjs).
|
|
49
|
+
if (lines.length) lines.push('One helper per role works on a brief. Only when the member asks for several of one role ("use 3 subagents", "dùng 2 helper"), run up to 3 in one delegate call: copies for independent answers on the same brief, or split for shares of the work, with member_words quoting the member\'s request. Never add helpers on your own.');
|
|
48
50
|
return lines.length ? ' ' + lines.join(' ') : '';
|
|
49
51
|
}
|
|
50
52
|
const HELPER_USES = {
|
|
@@ -52,10 +54,10 @@ const HELPER_USES = {
|
|
|
52
54
|
research: roles => roles.includes('scout') ? 'research: anything on the web (current information, news, library or API documentation, releases, error messages), with the code when needed. It searches and reads the pages; you get its summary with sources.'
|
|
53
55
|
: 'research: anything on the web (current information, news, documentation; it searches and reads the pages, you get its summary with sources), and before changing an area you do not know that spans more files than you can read in a few batches (where something is computed and every caller, how a flow crosses modules); your own context stays free for the change.',
|
|
54
56
|
verify: () => 'verify: an independent check of a result: it runs the repository checks on the current code (offline, and the project is read-only for its commands) and confirms claims, yours or a helper\'s, against the code and primary sources. It answers pass, fail or unverifiable per claim, with evidence.',
|
|
55
|
-
review: () => 'review: an independent review of the current patch (this turn\'s changes, commits made in the turn included). To review work committed in earlier turns, pass since: the commit, branch or HEAD~N it started from; when this turn changed nothing, it reviews the branch\'s commits since it left the default branch.',
|
|
57
|
+
review: () => 'review: an independent review of the current patch (this turn\'s changes, commits made in the turn included). To review work committed in earlier turns, pass since: the commit, branch or HEAD~N it started from; when this turn changed nothing, it reviews the branch\'s commits since it left the default branch. Ask once, when your change is finished, and again only after fixing blocking findings; under a required review the harness reviews the final patch and refuses your own call.',
|
|
56
58
|
};
|
|
57
59
|
export function delegateDescription(roles) {
|
|
58
|
-
return `Hand work to a helper with its own context. Helpers read the project but cannot change files or call helpers. ${roles.map(role => HELPER_USES[role](roles)).join(' ')} A lookup that one grep or read answers stays with you. Write the task as a brief the helper can act on alone: the goal, what to find or check, where to start and what you already know or ruled out, and the answer you need (file:line facts). One call per helper runs at a time; each can be called again, up to ${HELPER_CALLS} times per user message, for example a second
|
|
60
|
+
return `Hand work to a helper with its own context. Helpers read the project but cannot change files or call helpers. ${roles.map(role => HELPER_USES[role](roles)).join(' ')} A lookup that one grep or read answers stays with you. Write the task as a brief the helper can act on alone: the goal, what to find or check, where to start and what you already know or ruled out, and the answer you need (file:line facts). One call per helper runs at a time; each can be called again, up to ${HELPER_CALLS} times per user message, for example a second scout question on what the first answer opened.`;
|
|
59
61
|
}
|
|
60
62
|
// Scout and research end with their objections to the brief, if any.
|
|
61
63
|
const OBJECTIONS = `${BRIEF_CHECK} End your answer with one JSON object in a \`\`\`json block: {${BRIEF_ISSUES_JSON}}, with an empty list when the brief is sound. You cannot change files or delegate.`;
|
|
@@ -7,12 +7,14 @@ import { fenced, HELPER_UNTRUSTED_RULE } from './untrusted.mjs';
|
|
|
7
7
|
import { jobTitle } from './call-purpose.mjs';
|
|
8
8
|
import { ManagedToolBoundary } from './tool-boundary.mjs';
|
|
9
9
|
import { managedResourceLoader } from './resource-loader.mjs';
|
|
10
|
-
import { shareGrant, releaseGrant } from './grant-share.mjs';
|
|
10
|
+
import { shareGrant, releaseGrant, slotArgs, baseRole } from './grant-share.mjs';
|
|
11
11
|
import { describeFailure, failureCode, failureText } from '../runtime/managed-failure.mjs';
|
|
12
|
-
import { reviewParts, mergeReviews, REVIEW_PARALLEL, emptySnapshot, readBranchBase, readPatchDigest, readPatchSnapshot, resolveReviewBase } from './patch-snapshot.mjs';
|
|
13
|
-
import { reviewFindings, briefIssues } from './workflow.mjs';
|
|
12
|
+
import { reviewParts, focusParts, mergeReviews, REVIEW_PARALLEL, emptySnapshot, readBranchBase, readPatchDigest, readPatchSnapshot, resolveReviewBase } from './patch-snapshot.mjs';
|
|
13
|
+
import { reviewFindings, briefIssues, verifyVerdicts } from './workflow.mjs';
|
|
14
14
|
import { currentWorkflow, helperWorkflowPrompt } from './workflow-context.mjs';
|
|
15
15
|
import { anchorText, relayObjection, recordVerdicts } from './objections.mjs';
|
|
16
|
+
import { recordFindings } from './ledger.mjs';
|
|
17
|
+
import { copyNote, mergeCopies } from './copies.mjs';
|
|
16
18
|
import { HELPER_CALLS, HELPER_SETUP, READ_TOOLS, countedCheck } from './helper-roles.mjs';
|
|
17
19
|
import { trackToolFailures } from './tool-failures.mjs';
|
|
18
20
|
import { helperMemoryTools, renewer } from './memory.mjs';
|
|
@@ -43,14 +45,16 @@ async function reviewSnapshot(managed, harness, since) {
|
|
|
43
45
|
return branch ? Object.assign(await readPatchSnapshot(managed.boundary, null, branch.base), { from: { kind: 'branch', ref: branch.ref, base: branch.base } }) : snapshot;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
export async function runHelper(managed, role, task, signal, harness, { verifyGrant, patiently, title, since }) {
|
|
47
|
-
const snapshot = role === 'review' ? await reviewSnapshot(managed, harness, since) : null
|
|
48
|
+
export async function runHelper(managed, role, task, signal, harness, { verifyGrant, patiently, title, since, fan = null }) {
|
|
49
|
+
const snapshot = role === 'review' ? await reviewSnapshot(managed, harness, since) : null;
|
|
50
|
+
// The harness's re-review after a fix reads only the parts holding a file changed since (reviewFocus).
|
|
51
|
+
const parts = snapshot ? focusParts(reviewParts(snapshot), harness && !since ? managed.reviewFocus : null) : null;
|
|
48
52
|
// A helper whose company model account is out of usage, or resting after
|
|
49
53
|
// a refusal, starts once more: Studio then gives the new helper the next
|
|
50
54
|
// model of its role that serves (an auto role's list). Helpers read and
|
|
51
55
|
// run checks only, so starting over repeats no change.
|
|
52
56
|
for (let attempt = 1; ; attempt += 1) {
|
|
53
|
-
try { return await helperAttempt(managed, role, task, signal, harness, snapshot, parts, { verifyGrant, patiently, title }); }
|
|
57
|
+
try { return await helperAttempt(managed, role, task, signal, harness, snapshot, parts, { verifyGrant, patiently, title, fan }); }
|
|
54
58
|
catch (error) {
|
|
55
59
|
if (attempt >= HELPER_ATTEMPTS || signal?.aborted || !HELPER_SWITCH.has(failureCode(error?.message)) || (managed.helperCalls.get(role) ?? 0) >= HELPER_CALLS) throw error;
|
|
56
60
|
}
|
|
@@ -59,13 +63,62 @@ export async function runHelper(managed, role, task, signal, harness, { verifyGr
|
|
|
59
63
|
// A helper's role closes through the same wait as the run's close: a passing
|
|
60
64
|
// failure of Agent Watch's way to Studio is asked again. A close that still
|
|
61
65
|
// fails leaves the helper's answer standing: the role closes with the run,
|
|
62
|
-
// and the next helper of the role closes it first.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
// and the next helper of the role closes it first. `key`: the role, or
|
|
67
|
+
// role#slot for a helper the member asked for beside it (grant-share.mjs).
|
|
68
|
+
export async function closeRole(managed, key) {
|
|
69
|
+
try { await viaLink(managed.studioLink, () => managed.broker.request('close', slotArgs(key))); managed.unclosed?.delete(key); }
|
|
70
|
+
catch { (managed.unclosed ??= new Set()).add(key); }
|
|
66
71
|
}
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
// Helpers the member asked for run on grants of their own (slots) when
|
|
73
|
+
// Studio and Agent Watch hold several of a role at once; otherwise side by
|
|
74
|
+
// side on the role's one grant, where Studio takes their requests in turn.
|
|
75
|
+
const slotsReady = managed => (managed.manifest?.broker_features ?? []).includes('helper-slots');
|
|
76
|
+
|
|
77
|
+
async function helperAttempt(managed, role, task, signal, harness, snapshot, parts, { verifyGrant, patiently, title, fan }) {
|
|
78
|
+
const setup = HELPER_SETUP[role], opts = { verifyGrant, patiently, title };
|
|
79
|
+
let result;
|
|
80
|
+
if (!fan) result = await answerUnder(managed, role, task, signal, harness, snapshot, parts, opts, ['']);
|
|
81
|
+
else if (!slotsReady(managed)) result = await answerUnder(managed, role, task, signal, harness, snapshot, parts, opts, Array.from({ length: fan.n }, (_, i) => copyNote(role, i, fan)));
|
|
82
|
+
else {
|
|
83
|
+
// One grant per helper, all at once; the first that fails stops the others.
|
|
84
|
+
const stop = new AbortController(), both = AbortSignal.any([stop.signal, ...(signal ? [signal] : [])]);
|
|
85
|
+
let results;
|
|
86
|
+
try { results = await Promise.all(Array.from({ length: fan.n }, (_, i) => answerUnder(managed, i ? `${role}#${i + 1}` : role, task, both, harness, snapshot, parts, opts, [copyNote(role, i, fan)]).catch(error => { stop.abort(); throw error; }))); }
|
|
87
|
+
catch (error) { throw signal?.aborted ? Error('managed-helper-cancelled') : error; }
|
|
88
|
+
const merged = mergeCopies(role, setup.label, results, { findingsOf: reviewFindings, issuesOf: briefIssues, verdictsOf: verifyVerdicts });
|
|
89
|
+
result = { ...results[0], reply: merged.reply, usage: merged.usage, review: merged.findings ? merged : null, stale: results.some(r => r.stale), report: null };
|
|
90
|
+
}
|
|
91
|
+
const { reply, usage, review, stale, from, grant } = result;
|
|
92
|
+
const details = { role, runID: grant.run_id, tokens: usage, patchDigest: snapshot?.digest, stale, ...(fan ? { helpers: fan.n, ...(slotsReady(managed) ? { slots: true } : {}) } : {}), ...(from ? { since: { kind: from.kind, ref: from.ref, base: from.base } } : {}) };
|
|
93
|
+
if (snapshot) {
|
|
94
|
+
// Severity-graded findings decide whether the harness sends the agent back.
|
|
95
|
+
const findings = review ? review.findings : reviewFindings(reply);
|
|
96
|
+
Object.assign(details, { parsed: review ? review.parsed : !!findings, findings: (findings ?? []).slice(0, 40), blocking: findings?.filter(f => f.severity === 'blocking').length ?? 0 });
|
|
97
|
+
if (review) Object.assign(details, { parts: parts.parts.length, unread: parts.omitted.map(o => o.path).slice(0, 50) });
|
|
98
|
+
if (managed.run) { managed.run.reviews += 1; managed.run.blocking += details.blocking; }
|
|
99
|
+
// A review of earlier commits is not the review of this turn's change;
|
|
100
|
+
// what each file held then tells a later re-review what changed since.
|
|
101
|
+
if (!from) { managed.review = details; if (!stale) { recordFindings(managed, findings); managed.reviewFiles = await managed.changedFiles?.(); } }
|
|
102
|
+
managed.session.sessionManager.appendCustomEntry('agent-watch-review', details);
|
|
103
|
+
}
|
|
104
|
+
const verdicts = role === 'verify' ? recordVerdicts(managed, reply, harness, details) : '';
|
|
105
|
+
const report = result.report ?? answeredSettlement(role, reply, details, managed.helperSpecs?.get(role) ?? {}); details.outcome = report.outcome;
|
|
106
|
+
await managed.session.sendCustomMessage({ customType: 'agent-watch-helper-receipt', display: true,
|
|
107
|
+
content: `${setup.label}: ${usage.toLocaleString('en-US')} tokens${verdicts}${fan ? ` · ${fan.n} helpers` : review ? ` · ${parts.parts.length} parts` : ''}${snapshot ? (stale ? ' · review is stale' : ' · patch ' + snapshot.digest.slice(0, 12)) : ''} · ${OUTCOME_WORDS[report.outcome]}.`, details }, { triggerTurn: false });
|
|
108
|
+
// An objection to the brief comes with the answer; a stale review has none to act on.
|
|
109
|
+
const objection = stale ? '' : await relayObjection(managed, role, reply, harness);
|
|
110
|
+
const ending = `Outcome: ${report.outcome}${report.outcome === 'unverifiable' ? ` (${report.summary})` : ''}.\n\n`;
|
|
111
|
+
// The helper read untrusted content: its answer is data for the main agent.
|
|
112
|
+
return { content: [{ type: 'text', text: ending + (stale ? 'Review is stale: the patch changed during review. Ask for another review only after fixing blocking findings; under a required review the harness reviews the final patch.' : fenced(`${setup.label} helper's answer`, reply) + objection) }], details };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// One grant of the role (`key`: the role, or role#slot) and the helper
|
|
116
|
+
// sessions that answer under it, one per note (a copy's place among the
|
|
117
|
+
// helpers the member asked for). Its settlement goes to Studio and the grant
|
|
118
|
+
// closes before the answer is used.
|
|
119
|
+
async function answerUnder(managed, key, task, signal, harness, snapshot, parts, { verifyGrant, patiently, title }, notes) {
|
|
120
|
+
const role = baseRole(key);
|
|
121
|
+
if (managed.unclosed?.has(key)) await closeRole(managed, key);
|
|
69
122
|
// A long local tool step may outlive the main lease. Refresh authority
|
|
70
123
|
// before asking for a child; the broker serializes concurrent renewals,
|
|
71
124
|
// and main's own searches running beside it share managed renew.
|
|
@@ -75,18 +128,18 @@ async function helperAttempt(managed, role, task, signal, harness, snapshot, par
|
|
|
75
128
|
try {
|
|
76
129
|
const parent = await share.grant;
|
|
77
130
|
verifyGrant(parent, managed.manifest, 'main'); managed.grant = parent;
|
|
78
|
-
grant = await patiently(() => managed.broker.request('child',
|
|
131
|
+
grant = await patiently(() => managed.broker.request('child', slotArgs(key)), 2);
|
|
79
132
|
} finally { releaseGrant(managed, 'main', share); }
|
|
80
|
-
managed.helperCalls.set(role, (managed.helperCalls.get(role) ?? 0) +
|
|
81
|
-
try { verifyGrant(grant, managed.manifest,
|
|
82
|
-
catch (error) { try { await managed.broker.request('close',
|
|
133
|
+
managed.helperCalls.set(role, (managed.helperCalls.get(role) ?? 0) + notes.length); // each helper the member asked for counts
|
|
134
|
+
try { verifyGrant(grant, managed.manifest, key); }
|
|
135
|
+
catch (error) { try { await managed.broker.request('close', slotArgs(key)); } catch { /* closes with the run */ } throw error; }
|
|
83
136
|
} catch (error) { throw Error(`managed-helper-failed: ${describeFailure(role, failureText(error))}`); }
|
|
84
137
|
let boundary, report = null;
|
|
85
|
-
const sessions = new Set(), spec = managed.helperSpecs?.get(role) ?? {};
|
|
86
|
-
const renew = renewer(managed,
|
|
138
|
+
const sessions = new Set(), watched = new Set(), spec = managed.helperSpecs?.get(role) ?? {};
|
|
139
|
+
const renew = renewer(managed, key, g => verifyGrant(g, managed.manifest, key));
|
|
87
140
|
try {
|
|
88
141
|
const runtime = await managed.api.ModelRuntime.create({ credentials: new managed.ai.InMemoryCredentialStore(), modelsPath: null, refreshOnCreate: false, allowModelNetwork: false });
|
|
89
|
-
const model = managed.installModel(runtime, grant); managed.wrapStreams(runtime,
|
|
142
|
+
const model = managed.installModel(runtime, grant); managed.wrapStreams(runtime, key);
|
|
90
143
|
// Studio's logs name the job by its title (the brief's first line without one).
|
|
91
144
|
(managed.jobTitles ??= new WeakMap()).set(runtime, jobTitle(task, title));
|
|
92
145
|
const setup = HELPER_SETUP[role], memory = helperMemoryTools(managed, () => grant, renew);
|
|
@@ -95,15 +148,15 @@ async function helperAttempt(managed, role, task, signal, harness, snapshot, par
|
|
|
95
148
|
// One helper session answers the brief (with one part of the patch for a
|
|
96
149
|
// review in parts); its reply and tokens. A helper runs at its role's
|
|
97
150
|
// level from Studio (fixed by the Harness, or its main agent's).
|
|
98
|
-
const ask = async extra => {
|
|
151
|
+
const ask = async (extra, note = '', who = key) => {
|
|
99
152
|
const { session } = await managed.api.createAgentSession({ cwd: managed.cwd, model, modelRuntime: runtime, thinkingLevel: grant.effort || 'off',
|
|
100
153
|
settingsManager: managed.api.SettingsManager.inMemory({ retry: { enabled: false, provider: { maxRetries: 0 } }, cacheWarming: 'off' }),
|
|
101
154
|
sessionManager: managed.api.SessionManager.inMemory(managed.cwd), noTools: 'builtin', tools: [...READ_TOOLS, ...(setup.commands ? ['bash'] : []), ...(setup.web ? ['web_search', 'web_fetch'] : []), ...memory.map(tool => tool.name)],
|
|
102
155
|
customTools: [...boundary.tools(managed.api).filter(tool => READ_TOOLS.includes(tool.name) || setup.commands && tool.name === 'bash').map(tool => countedCheck(managed, tool)),
|
|
103
|
-
...(setup.web ? managed.webTools(runtime,
|
|
104
|
-
sessions.add(session); trackToolFailures(managed, session, role);
|
|
156
|
+
...(setup.web ? managed.webTools(runtime, key) : []), ...memory], resourceLoader: managedResourceLoader(managed.api, { systemPrompt: setup.prompt + HELPER_UNTRUSTED_RULE }) });
|
|
157
|
+
sessions.add(session); trackToolFailures(managed, session, role); watched.add(who); watchHelper(managed, who, session, task, title);
|
|
105
158
|
const abort = () => void session.abort(); signal?.addEventListener('abort', abort, { once: true });
|
|
106
|
-
try { if (signal?.aborted) throw Error('managed-helper-cancelled'); await session.prompt(anchorText(managed.session.sessionManager, harness) + task + helperWorkflowPrompt(currentWorkflow(managed.session.sessionManager)) + checks + extra, { expandPromptTemplates: false }); }
|
|
159
|
+
try { if (signal?.aborted) throw Error('managed-helper-cancelled'); await session.prompt(anchorText(managed.session.sessionManager, harness) + task + note + helperWorkflowPrompt(currentWorkflow(managed.session.sessionManager)) + checks + extra, { expandPromptTemplates: false }); }
|
|
107
160
|
finally { signal?.removeEventListener('abort', abort); }
|
|
108
161
|
const messages = session.messages.filter(m => m.role === 'assistant');
|
|
109
162
|
// The main agent (and the member) learn why a helper failed, not just that it did.
|
|
@@ -112,50 +165,65 @@ async function helperAttempt(managed, role, task, signal, harness, snapshot, par
|
|
|
112
165
|
if (!messages.length || messages.at(-1).stopReason === 'aborted') throw Error(signal?.aborted ? 'managed-helper-cancelled' : 'managed-helper-failed');
|
|
113
166
|
return { reply: messages.map(textContent).join('\n'), usage: messages.reduce((n, m) => n + (m.usage?.totalTokens ?? 0), 0) };
|
|
114
167
|
};
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
({
|
|
118
|
-
reply = reply.slice(0, 16000);
|
|
119
|
-
} else {
|
|
168
|
+
// One helper's answer: the brief (with the whole patch, or the patch in parts for a large review).
|
|
169
|
+
const once = async (note = '', who = key) => {
|
|
170
|
+
if (!parts || parts.parts.length === 1) { const answer = await ask(parts ? `\n${parts.parts[0].text}` : '', note, who); return { ...answer, reply: answer.reply.slice(0, 16000), review: null }; }
|
|
120
171
|
// A large patch: one reviewer per part, REVIEW_PARALLEL at a time (Studio
|
|
121
172
|
// admits one request of a role at a time); the first part that fails
|
|
122
173
|
// stops the others.
|
|
123
174
|
const answers = new Array(parts.parts.length);
|
|
124
175
|
let next = 0;
|
|
125
176
|
const reviewer = async () => {
|
|
126
|
-
while (next < parts.parts.length) { const i = next++; answers[i] = await ask(`\n${parts.parts[i].text}
|
|
177
|
+
while (next < parts.parts.length) { const i = next++; answers[i] = await ask(`\n${parts.parts[i].text}`, note, who); }
|
|
127
178
|
};
|
|
128
179
|
try { await Promise.all(Array.from({ length: Math.min(REVIEW_PARALLEL, parts.parts.length) }, reviewer)); }
|
|
129
180
|
catch (error) { next = parts.parts.length; for (const session of sessions) void session.abort(); throw error; }
|
|
130
|
-
review = mergeReviews(parts, answers, { findingsOf: reviewFindings, issuesOf: briefIssues });
|
|
131
|
-
|
|
181
|
+
const review = mergeReviews(parts, answers, { findingsOf: reviewFindings, issuesOf: briefIssues });
|
|
182
|
+
return { reply: review.reply, usage: review.usage, review };
|
|
183
|
+
};
|
|
184
|
+
let reply, usage, review = null;
|
|
185
|
+
if (notes.length === 1) ({ reply, usage, review } = await once(notes[0]));
|
|
186
|
+
else {
|
|
187
|
+
// Helpers the member asked for on this one grant, each on its own; the first that fails stops the others.
|
|
188
|
+
let answers;
|
|
189
|
+
try { answers = await Promise.all(notes.map((note, i) => once(note, i ? `${role}#${i + 1}` : key))); }
|
|
190
|
+
catch (error) { for (const session of sessions) void session.abort(); throw error; }
|
|
191
|
+
const merged = mergeCopies(role, setup.label, answers, { findingsOf: reviewFindings, issuesOf: briefIssues, verdictsOf: verifyVerdicts });
|
|
192
|
+
({ reply, usage } = merged); review = merged.findings ? merged : null;
|
|
132
193
|
}
|
|
133
194
|
const from = snapshot?.from ?? null;
|
|
134
195
|
const stale = snapshot ? (await (from ? readPatchDigest(managed.boundary, null, from.base) : managed.patchDigest())) !== snapshot.digest : false;
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const findings = review ? review.findings : reviewFindings(reply);
|
|
139
|
-
Object.assign(details, { parsed: review ? review.parsed : !!findings, findings: (findings ?? []).slice(0, 40), blocking: findings?.filter(f => f.severity === 'blocking').length ?? 0 });
|
|
140
|
-
if (review) Object.assign(details, { parts: parts.parts.length, unread: parts.omitted.map(o => o.path).slice(0, 50) });
|
|
141
|
-
if (managed.run) { managed.run.reviews += 1; managed.run.blocking += details.blocking; }
|
|
142
|
-
// A review of earlier commits is not the review of this turn's change.
|
|
143
|
-
if (!from) managed.review = details;
|
|
144
|
-
managed.session.sessionManager.appendCustomEntry('agent-watch-review', details);
|
|
145
|
-
}
|
|
146
|
-
const verdicts = role === 'verify' ? recordVerdicts(managed.run, reply, harness, details) : '';
|
|
147
|
-
report = answeredSettlement(role, reply, details, spec); details.outcome = report.outcome;
|
|
148
|
-
await managed.session.sendCustomMessage({ customType: 'agent-watch-helper-receipt', display: true,
|
|
149
|
-
content: `${setup.label}: ${usage.toLocaleString('en-US')} tokens${verdicts}${review ? ` · ${parts.parts.length} parts` : ''}${snapshot ? (stale ? ' · review is stale' : ' · patch ' + snapshot.digest.slice(0, 12)) : ''} · ${OUTCOME_WORDS[report.outcome]}.`, details }, { triggerTurn: false });
|
|
150
|
-
// An objection to the brief comes with the answer; a stale review has none to act on.
|
|
151
|
-
const objection = stale ? '' : await relayObjection(managed, role, reply, harness);
|
|
152
|
-
const ending = `Outcome: ${report.outcome}${report.outcome === 'unverifiable' ? ` (${report.summary})` : ''}.\n\n`;
|
|
153
|
-
// The helper read untrusted content: its answer is data for the main agent.
|
|
154
|
-
return { content: [{ type: 'text', text: ending + (stale ? 'Review is stale: the patch changed during review. Obtain a new review for the current patch.' : fenced(`${setup.label} helper's answer`, reply) + objection) }], details };
|
|
196
|
+
const findings = snapshot ? (review ? review.findings : reviewFindings(reply)) : null;
|
|
197
|
+
report = answeredSettlement(role, reply, snapshot ? { stale, parsed: review ? review.parsed : !!findings, findings: findings ?? [] } : {}, spec);
|
|
198
|
+
return { reply, usage, review, stale, from, grant, report };
|
|
155
199
|
} catch (error) { report = failedSettlement(role, error, signal, spec); throw error; }
|
|
156
200
|
finally {
|
|
157
201
|
for (const session of sessions) session.dispose(); await boundary?.dispose();
|
|
158
202
|
await settleHelper(managed, renew.lease, report); // with the role's current token, before it closes
|
|
159
|
-
await closeRole(managed,
|
|
203
|
+
await closeRole(managed, key);
|
|
204
|
+
for (const who of watched) helperState(managed, who, null);
|
|
160
205
|
}
|
|
161
206
|
}
|
|
207
|
+
|
|
208
|
+
// The helpers running now and what each is doing, told to the session's
|
|
209
|
+
// listeners (the terminal's subagent panel, the WebUI) as it changes.
|
|
210
|
+
export function helperState(managed, key, state) {
|
|
211
|
+
const states = managed.helperStates ??= new Map();
|
|
212
|
+
if (state) states.set(key, state); else states.delete(key);
|
|
213
|
+
managed.notify?.({ type: 'managed_helper_state', helpers: [...states].map(([k, s]) => ({ key: k, role: baseRole(k), slot: slotArgs(k).slot ?? 1, ...s })) });
|
|
214
|
+
}
|
|
215
|
+
// What a helper is doing, for the terminal's subagent panel: the job and its
|
|
216
|
+
// latest tool step, told to listeners as it changes (company-terminal.mjs).
|
|
217
|
+
function watchHelper(managed, key, session, task, title) {
|
|
218
|
+
const job = String(title || task.split('\n').find(line => line.trim()) || '').trim().slice(0, 120);
|
|
219
|
+
helperState(managed, key, { job, step: 'đọc brief' });
|
|
220
|
+
session.subscribe?.(event => {
|
|
221
|
+
if (event?.type !== 'tool_execution_start') return;
|
|
222
|
+
helperState(managed, key, { job, step: stepText(event.toolName, event.args) });
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
const STEP = { read: 'đọc', grep: 'tìm', find: 'tìm file', ls: 'xem thư mục', bash: 'chạy', web_search: 'tìm trên web', web_fetch: 'đọc trang', memory_search: 'tra team memory', memory_ask: 'hỏi team memory' };
|
|
226
|
+
export function stepText(tool, args = {}) {
|
|
227
|
+
const what = args?.path ?? args?.pattern ?? args?.command ?? args?.query ?? args?.url ?? '';
|
|
228
|
+
return `${STEP[tool] ?? tool}${what ? ' ' + String(what).replace(/\s+/g, ' ').slice(0, 80) : ''}`;
|
|
229
|
+
}
|
|
@@ -24,7 +24,11 @@ export function delegateParameters() {
|
|
|
24
24
|
title: { type: 'string', maxLength: 120, description: 'One line naming the job, for the company logs (for example "Find where login tokens are stored").' },
|
|
25
25
|
task: { type: 'string', minLength: 1, maxLength: 12000 },
|
|
26
26
|
...Object.fromEntries(Object.entries(SPEC_FIELDS).map(([name, description]) => [name, field(description)])),
|
|
27
|
-
since: { type: 'string', maxLength: 200, description: 'Review only: the commit, branch or HEAD~N the work to review started from (work committed in earlier turns).' }
|
|
27
|
+
since: { type: 'string', maxLength: 200, description: 'Review only: the commit, branch or HEAD~N the work to review started from (work committed in earlier turns).' },
|
|
28
|
+
// Several helpers of the role, only at the member's request (copies.mjs).
|
|
29
|
+
copies: { type: 'integer', minimum: 2, maximum: 3, description: 'Only when the member asked for several helpers of this role: how many (2 or 3) run side by side on this brief. Never on your own.' },
|
|
30
|
+
split: { type: 'array', minItems: 2, maxItems: 3, items: { type: 'string', minLength: 1, maxLength: 500 }, description: 'Instead of copies: one line per helper naming its share of the work, when the member asked for several and the work divides.' },
|
|
31
|
+
member_words: { type: 'string', maxLength: 300, description: 'With copies or split: the member\'s own words, verbatim, that ask for that many helpers.' } },
|
|
28
32
|
required: ['role', 'task'], additionalProperties: false };
|
|
29
33
|
}
|
|
30
34
|
export const SPEC_PROMPT = ' Give a helper target and acceptance at least (also change, constraints and ownership when they matter): it answers against them, and its ending is recorded with them.';
|