@piagent/platform 1.51.2 → 1.52.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 +19 -0
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/distribution-standard.md +6 -6
- package/docs/harness-conformance.md +13 -13
- 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-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 +29 -16
- 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 +39 -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 +29 -16
- 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 +10 -10
- package/packages/piagent-core/managed/general-clone.mjs +111 -0
- package/packages/piagent-core/managed/helper-roles.mjs +33 -42
- package/packages/piagent-core/managed/helper-run.mjs +21 -15
- package/packages/piagent-core/managed/helper-settlement.mjs +6 -10
- package/packages/piagent-core/managed/language-environment.mjs +6 -2
- package/packages/piagent-core/managed/ledger.mjs +11 -10
- package/packages/piagent-core/managed/objections.mjs +8 -25
- package/packages/piagent-core/managed/role-names.mjs +6 -0
- package/packages/piagent-core/managed/session.mjs +13 -13
- package/packages/piagent-core/managed/tool-boundary.mjs +4 -4
- package/packages/piagent-core/managed/workflow.mjs +2 -39
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/managed-failure.d.mts +1 -1
- package/packages/piagent-core/runtime/managed-failure.mjs +3 -3
- package/packages/piagent-webui/dist/client/assets/{App-C9tlGPMb.js → App-DQwj6l-F.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{GatewayApp-Dvo96qoX.js → GatewayApp-CP8GpZsK.js} +3 -3
- package/packages/piagent-webui/dist/client/assets/{InspectionApp-CGgHsOyC.js → InspectionApp-BZqu1pjw.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{SessionAgentWorkspace-n8UMMcrP.js → SessionAgentWorkspace-4nJSbzE9.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{SessionInspectorDrawer-B62kkr8J.js → SessionInspectorDrawer-DEb_TBE8.js} +2 -2
- package/packages/piagent-webui/dist/client/assets/SettingsNavigation-BF4X5xkR.js +3 -0
- package/packages/piagent-webui/dist/client/assets/{SettingsPage-DKg0SfZQ.js → SettingsPage-CS8LsdhS.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{TerminalPanel-BPTVsNjP.js → TerminalPanel-CiF2_woF.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{WorkspaceCodeEditor-BHGFFBc2.js → WorkspaceCodeEditor-OFxSEbBs.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{WorkspaceExplorer-nd4kghwK.js → WorkspaceExplorer-ZYIi-FWr.js} +3 -3
- package/packages/piagent-webui/dist/client/assets/{index-BiLVX8S-.js → index-CfJC4Unf.js} +2 -2
- package/packages/piagent-webui/dist/client/index.html +1 -1
- package/packages/piagent-webui/server/transcript-projection.ts +3 -3
- package/packages/piagent-webui/shared/company-copy.ts +3 -1
- package/packages/piagent-webui/shared/turn-end.ts +1 -1
- package/schemas/piagent-webui/transcript-v1.schema.json +3 -3
- package/scripts/runtime-advisory-exceptions.json +27 -27
- package/packages/piagent-webui/dist/client/assets/SettingsNavigation-BZijmuPt.js +0 -3
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.
|
|
40
|
+
<a class="version-badge" href="/whats-new" data-docs-version="v1.52.0" title="v1.52.0 docs · VI"><span class="dot"></span>v1.52.0</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,21 @@
|
|
|
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-
|
|
111
|
+
<article class="release latest" id="v1-52-0" data-release>
|
|
112
|
+
<div class="release-meta"><a class="release-version" href="#v1-52-0">v1.52.0</a><time class="release-date" datetime="2026-11-08">08/11/2026</time></div>
|
|
113
|
+
<div class="release-body">
|
|
114
|
+
<h3 data-toc-skip id="v1-52-0-title">Helper công ty có thêm advisor và general làm việc trong bản clone riêng</h3>
|
|
115
|
+
<p>Harness công ty khai báo mọi helper, và agent chính gọi chúng bằng tên công việc: explore, scout, advisor, general và review. General chỉ sửa code trong bản clone riêng và trả về một git ref để merge.</p>
|
|
116
|
+
<ul class="release-items">
|
|
117
|
+
<li data-kind="breaking"><span class="kind kind-breaking">Thay đổi lớn</span><span>Helper được gọi là explore, scout, advisor, general và review; tên cũ research và verify không còn, helper verify bị bỏ.</span></li>
|
|
118
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>advisor cho ý kiến thứ hai trước quyết định khó: khuyến nghị, các phương án đã loại và rủi ro.</span></li>
|
|
119
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>general nhận việc lớn trên macOS: sửa, chạy check và commit trong clone riêng, không đụng file của bạn, rồi trả về refs/general/<id>.</span></li>
|
|
120
|
+
<li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>WebUI ghi đúng lỗi của advisor hay general thay vì gọi là lỗi của review.</span></li>
|
|
121
|
+
</ul>
|
|
122
|
+
<p class="release-requires"><strong>Cần:</strong> Agent Studio 0.51.0 và Agent Watch 0.15.20 (Windows 0.1.19) để dùng helper advisor và general</p>
|
|
123
|
+
</div>
|
|
124
|
+
</article>
|
|
125
|
+
<article class="release" id="v1-51-2" data-release>
|
|
112
126
|
<div class="release-meta"><a class="release-version" href="#v1-51-2">v1.51.2</a><time class="release-date" datetime="2026-11-03">03/11/2026</time></div>
|
|
113
127
|
<div class="release-body">
|
|
114
128
|
<h3 data-toc-skip id="v1-51-2-title">Chạy riêng phần test liên quan dùng được với mọi công cụ test</h3>
|
|
@@ -408,25 +422,24 @@
|
|
|
408
422
|
</ul>
|
|
409
423
|
</div>
|
|
410
424
|
</article>
|
|
411
|
-
<article class="release" id="v1-44-0" data-release>
|
|
412
|
-
<div class="release-meta"><a class="release-version" href="#v1-44-0">v1.44.0</a><time class="release-date" datetime="2026-10-28">28/10/2026</time></div>
|
|
413
|
-
<div class="release-body">
|
|
414
|
-
<h3 data-toc-skip id="v1-44-0-title">Giữ Pi của bạn, chọn giao diện Terminal, thêm repo skill <span class="tag">không phát hành</span></h3>
|
|
415
|
-
<p>Đã tag nhưng chưa publish: advisory undici mới trong Pi host làm kiểm tra runtime fail. Thay bằng 1.44.1.</p>
|
|
416
|
-
<ul class="release-items">
|
|
417
|
-
<li data-kind="new"><span class="kind kind-new">Mới</span><span>/style chọn giao diện Terminal: system, dark, light của Pi hoặc ocean, paper, contrast, focus của Piagent. Terminal công ty nhớ lựa chọn.</span></li>
|
|
418
|
-
<li data-kind="new"><span class="kind kind-new">Mới</span><span>piagent skills add mattpocock/skills thêm repo skill GitHub ghim một commit; skill dùng được dạng /skill:tên trong Pi, Terminal công ty và dashboard.</span></li>
|
|
419
|
-
<li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Setup, install, update không còn hạ pi mới hơn của dev (vd 1.4.0) về bản pin 1.3.0: Piagent giữ bản riêng trong ~/.piagent/pi-host cho phiên công ty, dashboard và lệnh piagent pi.</span></li>
|
|
420
|
-
<li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>piagent-uninstall --with-host gỡ bản Pi riêng của Piagent và giữ nguyên pi mới hơn của dev.</span></li>
|
|
421
|
-
</ul>
|
|
422
|
-
</div>
|
|
423
|
-
</article>
|
|
424
425
|
</section>
|
|
425
426
|
<section class="section archive">
|
|
426
427
|
<h2 id="archive">Các bản trước</h2>
|
|
427
428
|
<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
429
|
<details class="release-archive">
|
|
429
|
-
<summary><span>Tháng 10/2026</span><span class="archive-count">
|
|
430
|
+
<summary><span>Tháng 10/2026</span><span class="archive-count">75</span></summary>
|
|
431
|
+
<div class="archive-row" id="v1-44-0">
|
|
432
|
+
<div class="archive-head"><a class="release-version" href="#v1-44-0">v1.44.0</a> <span class="release-date">28/10/2026</span></div>
|
|
433
|
+
<div class="archive-body">
|
|
434
|
+
<p><strong>Giữ Pi của bạn, chọn giao diện Terminal, thêm repo skill</strong> <span class="tag">không phát hành</span> — Đã tag nhưng chưa publish: advisory undici mới trong Pi host làm kiểm tra runtime fail. Thay bằng 1.44.1.</p>
|
|
435
|
+
<ul class="release-items">
|
|
436
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>/style chọn giao diện Terminal: system, dark, light của Pi hoặc ocean, paper, contrast, focus của Piagent. Terminal công ty nhớ lựa chọn.</span></li>
|
|
437
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>piagent skills add mattpocock/skills thêm repo skill GitHub ghim một commit; skill dùng được dạng /skill:tên trong Pi, Terminal công ty và dashboard.</span></li>
|
|
438
|
+
<li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Setup, install, update không còn hạ pi mới hơn của dev (vd 1.4.0) về bản pin 1.3.0: Piagent giữ bản riêng trong ~/.piagent/pi-host cho phiên công ty, dashboard và lệnh piagent pi.</span></li>
|
|
439
|
+
<li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>piagent-uninstall --with-host gỡ bản Pi riêng của Piagent và giữ nguyên pi mới hơn của dev.</span></li>
|
|
440
|
+
</ul>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
430
443
|
<div class="archive-row" id="v1-43-9">
|
|
431
444
|
<div class="archive-head"><a class="release-version" href="#v1-43-9">v1.43.9</a> <span class="release-date">27/10/2026</span></div>
|
|
432
445
|
<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.
|
|
40
|
+
<a class="version-badge" href="/whats-new" data-docs-version="v1.52.0" title="v1.52.0 docs · VI"><span class="dot"></span>v1.52.0</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.
|
|
40
|
+
<a class="version-badge" href="/whats-new" data-docs-version="v1.52.0" title="v1.52.0 docs · VI"><span class="dot"></span>v1.52.0</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": 653,
|
|
260
|
+
"sha256": "577758e269e6cb8d79d32cec4c354be0ea343404c897c24280dc50f86002ea21"
|
|
261
261
|
}
|
|
262
262
|
],
|
|
263
263
|
"custodianComparisonRules": {
|
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { callName } from './role-names.mjs';
|
|
1
2
|
import { memberMessages, memberRequest } from './objections.mjs';
|
|
2
3
|
|
|
3
4
|
// Several helpers of one role, at the member's request only ("use 3
|
|
@@ -30,7 +31,7 @@ export function fanOut(manager, role, args, harness) {
|
|
|
30
31
|
if (harness) throw Error('managed-copies-invalid: the harness runs one helper per call.');
|
|
31
32
|
const words = normal(args?.member_words);
|
|
32
33
|
if (words.length < 3 || !memberRequest(memberMessages(manager)).some(text => normal(text).includes(words)) || namedCount(words) < n || !HELPER_WORDS.test(words)) return { dropped: n };
|
|
33
|
-
if (n > MAX_COPIES) throw Error(`managed-copies-invalid: ask for 2 to ${MAX_COPIES} ${role} helpers (copies, or one split line per helper).`);
|
|
34
|
+
if (n > MAX_COPIES) throw Error(`managed-copies-invalid: ask for 2 to ${MAX_COPIES} ${callName(role)} helpers (copies, or one split line per helper).`);
|
|
34
35
|
return { n, split: split.length ? split : null };
|
|
35
36
|
}
|
|
36
37
|
// The helper's answer, with what the main agent reads above it when it asked
|
|
@@ -38,12 +39,12 @@ export function fanOut(manager, role, args, harness) {
|
|
|
38
39
|
export function withDroppedNote(result, role, asked) {
|
|
39
40
|
const n = asked?.dropped, first = result?.content?.[0];
|
|
40
41
|
if (!n || first?.type !== 'text') return result;
|
|
41
|
-
const note = `One ${role} helper ran, not ${n}: the member's own words did not ask for ${n} ${role} helpers. Go on with this answer; run several only when the member asks for that many.\n\n`;
|
|
42
|
+
const note = `One ${callName(role)} helper ran, not ${n}: the member's own words did not ask for ${n} ${callName(role)} helpers. Go on with this answer; run several only when the member asks for that many.\n\n`;
|
|
42
43
|
return { ...result, content: [{ ...first, text: note + first.text }, ...result.content.slice(1)] };
|
|
43
44
|
}
|
|
44
45
|
// What one copy reads under its brief.
|
|
45
46
|
export function copyNote(role, i, fan) {
|
|
46
|
-
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.'}`;
|
|
47
|
+
return `\n\nThe member asked for ${fan.n} ${callName(role)} helpers; you are ${callName(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.'}`;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
const jsonOf = reply => {
|
|
@@ -53,18 +54,17 @@ const jsonOf = reply => {
|
|
|
53
54
|
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; }); };
|
|
54
55
|
const SEVERITY = { blocking: 0, major: 1, minor: 2 };
|
|
55
56
|
// One answer from the copies: each helper's prose (its JSON left out), then
|
|
56
|
-
// one JSON block joining their findings
|
|
57
|
-
// review
|
|
58
|
-
// gave its JSON. A finding
|
|
59
|
-
export function mergeCopies(role, label, answers, { findingsOf, issuesOf
|
|
57
|
+
// one JSON block joining their findings and objections, so the
|
|
58
|
+
// review and objection readers see every copy. `parsed`: every copy
|
|
59
|
+
// gave its JSON. A finding any copy reports stands.
|
|
60
|
+
export function mergeCopies(role, label, answers, { findingsOf, issuesOf }) {
|
|
60
61
|
const n = answers.length, room = Math.floor(12000 / n);
|
|
61
62
|
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;
|
|
62
|
-
const verdicts = role === 'verify' ? unique(answers.flatMap(a => verdictsOf(a.reply) ?? []), v => `${v.status}|${v.claim}`) : null;
|
|
63
63
|
const issues = unique(answers.flatMap(a => issuesOf(a.reply) ?? []), i => `${i.kind}|${i.detail}`).slice(0, 5);
|
|
64
64
|
const parsed = answers.every(a => jsonOf(a.reply));
|
|
65
65
|
const summaries = answers.map(a => jsonOf(a.reply)?.summary).filter(s => typeof s === 'string').map(s => s.slice(0, 200));
|
|
66
|
-
const json = { ...(findings ? { findings: findings.slice(0, 40) } : {}),
|
|
67
|
-
summary: `${n} ${role} helpers${summaries.length ? ': ' + summaries.join(' | ') : ''}`.slice(0, 900) };
|
|
66
|
+
const json = { ...(findings ? { findings: findings.slice(0, 40) } : {}), brief_issues: issues,
|
|
67
|
+
summary: `${n} ${callName(role)} helpers${summaries.length ? ': ' + summaries.join(' | ') : ''}`.slice(0, 900) };
|
|
68
68
|
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');
|
|
69
69
|
return { reply: `${body}\n\n\`\`\`json\n${JSON.stringify(json)}\n\`\`\``, usage: answers.reduce((total, a) => total + a.usage, 0),
|
|
70
70
|
...(findings ? { findings, parsed, blocking: findings.filter(f => f.severity === 'blocking').length } : {}) };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { cacheDirectory } from './language-environment.mjs';
|
|
8
|
+
|
|
9
|
+
// The general helper changes files, so it never works in the member's
|
|
10
|
+
// project. It works in its own clone: a separate repository with its own
|
|
11
|
+
// .git (refs, config, hooks), no remote, and no writable path to the project
|
|
12
|
+
// or its refs. A linked worktree would share the project's .git, and the
|
|
13
|
+
// sandbox opens a shared git directory for writing. The clone is `--shared`: it
|
|
14
|
+
// borrows the project's object files (read-only inside the sandbox, by the
|
|
15
|
+
// boundary's readRoots) instead of copying them, so it costs about what a
|
|
16
|
+
// worktree costs. The only crossing is takeBack below, run by this runtime,
|
|
17
|
+
// not by the helper: it copies the clone's commits into the project as one
|
|
18
|
+
// private ref, refs/general/<id>, and touches nothing else. The main agent
|
|
19
|
+
// merges that ref itself.
|
|
20
|
+
export const GENERAL_ENTRY = 'agent-watch-general';
|
|
21
|
+
const OWNER = '.piagent-owner';
|
|
22
|
+
const run = promisify(execFile);
|
|
23
|
+
// The clone's own configuration is the helper's to write. The runtime
|
|
24
|
+
// therefore reads it only through git commands that cannot start a program
|
|
25
|
+
// the clone names: these override what a clone's config could set.
|
|
26
|
+
const SAFE = ['-c', 'core.fsmonitor=false', '-c', 'core.hooksPath=/dev/null', '-c', 'core.alternateRefsCommand=', '-c', 'protocol.file.allow=always'];
|
|
27
|
+
const gitEnv = () => ({ PATH: '/usr/bin:/bin', HOME: os.tmpdir(), GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_NOSYSTEM: '1', GIT_TERMINAL_PROMPT: '0', LANG: 'en_US.UTF-8' });
|
|
28
|
+
const git = (exe, cwd, args) => run(exe, [...SAFE, ...args], { cwd, env: gitEnv(), maxBuffer: 4 << 20, timeout: 120_000 }).then(r => r.stdout.trim());
|
|
29
|
+
|
|
30
|
+
// A clone of the project's HEAD. Work not yet committed in the project is not
|
|
31
|
+
// in it: `dirty` lists those paths so the main agent can be told.
|
|
32
|
+
export async function cloneProject(managed) {
|
|
33
|
+
// The clone's isolation is proven under the macOS sandbox only; the Linux (bubblewrap) path is not, so it stays closed.
|
|
34
|
+
if (process.platform !== 'darwin') throw Error('managed-general-unavailable: the general helper is not verified on this platform yet; do the work yourself.');
|
|
35
|
+
const exe = managed.boundary.git, project = managed.cwd;
|
|
36
|
+
let top, base, dirty, objects;
|
|
37
|
+
try {
|
|
38
|
+
top = fs.realpathSync(await git(exe, project, ['rev-parse', '--show-toplevel']));
|
|
39
|
+
base = await git(exe, project, ['rev-parse', '--verify', 'HEAD']);
|
|
40
|
+
objects = fs.realpathSync(path.resolve(project, await git(exe, project, ['rev-parse', '--git-path', 'objects'])));
|
|
41
|
+
dirty = (await git(exe, project, ['status', '--porcelain', '-uall'])).split('\n').filter(Boolean).map(l => l.replace(/^\S{1,2}\s+/, '')).slice(0, 50);
|
|
42
|
+
} catch { throw Error('managed-general-needs-git: the general helper needs the project to be a git repository with a commit; commit first or do the work yourself.'); }
|
|
43
|
+
if (top !== project) throw Error('managed-general-needs-git: the general helper needs the project folder to be the repository root.');
|
|
44
|
+
const id = randomUUID().slice(0, 8), dir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'piagent-general-'));
|
|
45
|
+
try {
|
|
46
|
+
await git(exe, dir, ['clone', '-q', '--shared', '--no-tags', '--template=', '--', project, path.join(dir, 'work')]);
|
|
47
|
+
await git(exe, path.join(dir, 'work'), ['remote', 'remove', 'origin']);
|
|
48
|
+
} catch (error) { fs.rmSync(dir, { recursive: true, force: true }); throw Error(`managed-general-clone-failed: ${String(error?.message).slice(0, 200)}`); }
|
|
49
|
+
const token = randomUUID(); // the proof, in the folder and in the session's record, that this runtime made it
|
|
50
|
+
fs.writeFileSync(path.join(dir, OWNER), token, { mode: 0o600 });
|
|
51
|
+
const real = fs.realpathSync(path.join(dir, 'work'));
|
|
52
|
+
return { id, base, dirty, token, objects, cache: cacheDirectory(managed.boundary.userHome, real), root: dir, dir: real };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The clone's folder, and the package cache the sandbox keyed to it (no later run shares it).
|
|
56
|
+
export function discardClone(clone) { fs.rmSync(clone.root, { recursive: true, force: true }); fs.rmSync(clone.cache, { recursive: true, force: true }); }
|
|
57
|
+
|
|
58
|
+
// Bring the clone's commits into the project as refs/general/<id> and
|
|
59
|
+
// discard the clone. Returns what the main agent needs to merge them.
|
|
60
|
+
export async function takeBack(managed, clone) {
|
|
61
|
+
const exe = managed.boundary.git, project = managed.cwd;
|
|
62
|
+
try {
|
|
63
|
+
// The helper writes the clone's .git: a gitfile or alternates would point
|
|
64
|
+
// the fetch at a path outside it.
|
|
65
|
+
const dotgit = path.join(clone.dir, '.git');
|
|
66
|
+
const alternates = path.join(dotgit, 'objects/info/alternates');
|
|
67
|
+
if (!fs.lstatSync(dotgit).isDirectory() || fs.readFileSync(alternates, 'utf8').trim() !== clone.objects) throw Error('the clone was changed to point outside itself');
|
|
68
|
+
const ref = `refs/general/${clone.id}`;
|
|
69
|
+
await git(exe, project, ['fetch', '-q', '--no-tags', '--no-recurse-submodules', '--no-write-fetch-head', clone.dir, `+HEAD:${ref}`]);
|
|
70
|
+
const head = await git(exe, project, ['rev-parse', '--verify', ref]);
|
|
71
|
+
if (head === clone.base) { await git(exe, project, ['update-ref', '-d', ref]); return { ref: null, head, base: clone.base, files: [], note: 'The general helper made no commit, so nothing came back. Only committed work is returned.' }; }
|
|
72
|
+
if (!(await git(exe, project, ['merge-base', '--is-ancestor', clone.base, head]).then(() => true, () => false))) { await git(exe, project, ['update-ref', '-d', ref]); throw Error('the clone no longer descends from the commit it started from'); }
|
|
73
|
+
const files = (await git(exe, project, ['diff', '--numstat', `${clone.base}..${head}`])).split('\n').filter(Boolean).map(l => { const [added, deleted, ...name] = l.split('\t'); return `+${added} -${deleted} ${name.join('\t')}`; }).slice(0, 80);
|
|
74
|
+
return { ref, head, base: clone.base, files, note: null };
|
|
75
|
+
} finally { discardClone(clone); }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// What the main agent reads with the helper's answer.
|
|
79
|
+
export function takeBackText(clone, back) {
|
|
80
|
+
const dirty = clone.dirty.length ? ` The helper did not see ${clone.dirty.length} uncommitted path(s) in the project (${clone.dirty.slice(0, 8).join(', ')}${clone.dirty.length > 8 ? ', …' : ''}).` : '';
|
|
81
|
+
if (!back.ref) return `\n\n${back.note}${dirty}`;
|
|
82
|
+
return `\n\nThe general helper's commits are in ${back.ref} (${back.base.slice(0, 9)}..${back.head.slice(0, 9)}); your files are unchanged. Merge or cherry-pick that ref yourself, run the checks on the result, then ask for review; delete the ref afterwards (git update-ref -d ${back.ref}). Changed:\n${back.files.join('\n')}${dirty}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// One entry per general run, so a later turn knows what ran and what came back;
|
|
86
|
+
// "running" with the folder and its token is also the record of what to recover.
|
|
87
|
+
export function recordGeneral(managed, clone, status, back = null) {
|
|
88
|
+
managed.session.sessionManager.appendCustomEntry(GENERAL_ENTRY, { id: clone.id, base: clone.base, status, root: clone.root, token: clone.token, ...(back?.ref ? { ref: back.ref, head: back.head } : {}), at: new Date().toISOString() });
|
|
89
|
+
}
|
|
90
|
+
// A crash or a kill skips the cleanup above. When the conversation next runs a
|
|
91
|
+
// general helper, a run its record still calls "running" (and this process
|
|
92
|
+
// is not running) has its clone removed only if the folder is provably the one
|
|
93
|
+
// this runtime made: a real folder, ours, directly in the temporary folder,
|
|
94
|
+
// holding the token the record names. Anything else is only reported.
|
|
95
|
+
export function sweepGeneral(managed) {
|
|
96
|
+
const last = new Map(), tmp = fs.realpathSync(os.tmpdir()), removed = [], unproven = [];
|
|
97
|
+
for (const e of managed.session.sessionManager.getEntries?.() ?? []) if (e.type === 'custom' && e.customType === GENERAL_ENTRY) last.set(e.data.id, e.data);
|
|
98
|
+
for (const d of last.values()) {
|
|
99
|
+
if (d.status !== 'running' || typeof d.root !== 'string' || managed.liveClones?.has(d.root)) continue;
|
|
100
|
+
let st; try { st = fs.lstatSync(d.root); } catch { continue; } // already gone
|
|
101
|
+
let owned = false;
|
|
102
|
+
try {
|
|
103
|
+
owned = st.isDirectory() && !st.isSymbolicLink() && st.uid === process.getuid() && path.dirname(d.root) === tmp && path.basename(d.root).startsWith('piagent-general-')
|
|
104
|
+
&& fs.readFileSync(path.join(d.root, OWNER), 'utf8') === d.token;
|
|
105
|
+
} catch { /* no marker: not proven */ }
|
|
106
|
+
if (owned) { fs.rmSync(d.root, { recursive: true, force: true }); removed.push(d.id); } else unproven.push(d.root);
|
|
107
|
+
}
|
|
108
|
+
return { removed, unproven };
|
|
109
|
+
}
|
|
110
|
+
export const sweepText = ({ removed, unproven }) => (removed.length ? `\n\nAn earlier general run (${removed.join(', ')}) was interrupted; its clone folder was removed and any work in it was not returned.` : '')
|
|
111
|
+
+ (unproven.length ? `\n\nA folder an earlier general run recorded could not be proven to be the one it made, so it was left in place: ${unproven.join(', ')}.` : '');
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import { REVIEW_PROMPT,
|
|
1
|
+
import { REVIEW_PROMPT, BRIEF_CHECK, BRIEF_ISSUES_JSON } from './workflow.mjs';
|
|
2
2
|
|
|
3
3
|
// Each helper role runs at most this often per user message (Studio's MaxHelperCalls).
|
|
4
4
|
export const HELPER_CALLS = 8;
|
|
5
5
|
|
|
6
6
|
// The helpers a main agent may delegate to, in the Harness's order. Each is
|
|
7
7
|
// optional: only those the company Harness enables (and Agent Watch passes
|
|
8
|
-
// on) are offered.
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
8
|
+
// on) are offered. The main agent names a helper by its working name; Studio
|
|
9
|
+
// knows the same helper by its Harness role (its grant). explore reads the
|
|
10
|
+
// code, scout reads the web (and the code), advisor gives a second opinion,
|
|
11
|
+
// review judges the patch. general is the only one that changes files, and
|
|
12
|
+
// only in its own clone of the project (general-clone.mjs). Main does small
|
|
13
|
+
// changes, integration and the repository checks itself.
|
|
14
|
+
import { ROLE_NAMES, HELPER_ROLES, callName } from './role-names.mjs';
|
|
15
|
+
export { ROLE_NAMES, HELPER_ROLES, callName };
|
|
12
16
|
export const helperRoles = manifest => HELPER_ROLES.filter(role => manifest?.harness?.configuration?.[role]);
|
|
13
17
|
const WIDE_READING = 'when understanding the task needs more reading than a few batches of files, such as a flow across many modules of an unfamiliar codebase';
|
|
14
18
|
const NETWORK_COMMANDS = 'run_with_network only when a command itself needs the internet or starts a local server or browser, such as end-to-end tests (the user approves each one)';
|
|
@@ -24,7 +28,7 @@ export function mainWebLookups(manifest) {
|
|
|
24
28
|
export function limitedWebTools(tools, currentRun, limit) {
|
|
25
29
|
return tools.map(tool => ({ ...tool, execute: async (...args) => {
|
|
26
30
|
const run = currentRun();
|
|
27
|
-
if (run && ++run.webLookups > limit) throw Error(`managed-web-lookups-spent: you used your ${limit} quick web lookup(s) for this message. Hand the rest of the web work to the
|
|
31
|
+
if (run && ++run.webLookups > limit) throw Error(`managed-web-lookups-spent: you used your ${limit} quick web lookup(s) for this message. Hand the rest of the web work to the scout helper (delegate role "scout").`);
|
|
28
32
|
return tool.execute(...args);
|
|
29
33
|
} }));
|
|
30
34
|
}
|
|
@@ -34,15 +38,16 @@ export function webPrompt(roles, lookups = 0) {
|
|
|
34
38
|
if (!roles.includes('research'))
|
|
35
39
|
return ` Shell commands run without network: use web_search for current information or documentation, web_fetch to read a public https page, and ${NETWORK_COMMANDS}.`;
|
|
36
40
|
return lookups > 0
|
|
37
|
-
? ` Shell commands run without network. For one quick fact (a version, a flag, one documentation page) use web_search or web_fetch yourself, at most ${lookups} call(s) per member message; hand anything wider on the web (several sources, comparisons, news, research) to the
|
|
38
|
-
: ` Shell commands run without network and you have no web tools: hand anything on the web (current information, news, documentation, releases, error messages) to the
|
|
41
|
+
? ` Shell commands run without network. For one quick fact (a version, a flag, one documentation page) use web_search or web_fetch yourself, at most ${lookups} call(s) per member message; hand anything wider on the web (several sources, comparisons, news, research) to the scout helper, and use ${NETWORK_COMMANDS}.`
|
|
42
|
+
: ` Shell commands run without network and you have no web tools: hand anything on the web (current information, news, documentation, releases, error messages) to the scout helper, and use ${NETWORK_COMMANDS}.`;
|
|
39
43
|
}
|
|
40
44
|
export function helperPrompt(roles) {
|
|
41
45
|
const lines = [];
|
|
42
|
-
if (roles.includes('scout')) lines.push(`Use the
|
|
43
|
-
if (roles.includes('research')) lines.push(roles.includes('scout') ? 'Use the
|
|
44
|
-
: `Use the
|
|
45
|
-
if (roles.includes('
|
|
46
|
+
if (roles.includes('scout')) lines.push(`Use the explore helper (delegate role "explore") ${WIDE_READING}, and work from its file:line findings; do small lookups yourself.`);
|
|
47
|
+
if (roles.includes('research')) lines.push(roles.includes('scout') ? 'Use the scout helper (delegate role "scout") for anything on the web (current information, news, library and API docs, releases, error messages), together with the code when needed; give it the question and the answer you need, and work from its summary.'
|
|
48
|
+
: `Use the scout helper (delegate role "scout") for anything on the web (current information, news, documentation) and ${WIDE_READING}; work from its findings and do small code lookups yourself.`);
|
|
49
|
+
if (roles.includes('advisor')) lines.push('Use the advisor helper (delegate role "advisor") for a second opinion before a hard or hard-to-reverse decision (a design, a plan, a suspected root cause): give it the question, your options and what you know; it answers with a recommendation and the risks.');
|
|
50
|
+
if (roles.includes('general')) lines.push('Make small changes yourself. Use the general helper (delegate role "general") for large work, or work that can proceed apart from you: it works in its own clone of the repository, one general helper at a time, and cannot touch your files. It sees only committed work (commit first if it needs your changes); its commits come back as a git ref that you merge or cherry-pick yourself. You then run the repository checks and, for a non-trivial change, ask the review helper.');
|
|
46
51
|
// Helpers read the member's request beside the brief and may object to it.
|
|
47
52
|
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
53
|
// More than one helper of a role, only at the member's word (copies.mjs).
|
|
@@ -50,44 +55,30 @@ export function helperPrompt(roles) {
|
|
|
50
55
|
return lines.length ? ' ' + lines.join(' ') : '';
|
|
51
56
|
}
|
|
52
57
|
const HELPER_USES = {
|
|
53
|
-
scout: () => '
|
|
54
|
-
research: roles => roles.includes('scout') ? '
|
|
55
|
-
: '
|
|
56
|
-
|
|
58
|
+
scout: () => 'explore: 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). It reads only the project, no web, and answers with file:line facts; your own context stays free for the change.',
|
|
59
|
+
research: roles => roles.includes('scout') ? 'scout: 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.'
|
|
60
|
+
: 'scout: 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.',
|
|
61
|
+
advisor: () => 'advisor: a second opinion on a hard or hard-to-reverse decision. It reads the project, has no web, and answers with a recommendation, the options it rejected and the risks.',
|
|
62
|
+
general: () => 'general: large work, or work that can proceed apart from you. It edits and runs checks in its own clone of the repository (never your files), commits there, and its commits come back as the git ref it names, which you merge or cherry-pick yourself. It sees only committed work. One at a time.',
|
|
57
63
|
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.',
|
|
58
64
|
};
|
|
59
65
|
export function delegateDescription(roles) {
|
|
60
|
-
return `Hand work to a helper with its own context. Helpers read the project
|
|
66
|
+
return `Hand work to a helper with its own context. Helpers read the project and cannot call helpers; only general changes files, in its own clone. ${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.`;
|
|
61
67
|
}
|
|
62
|
-
//
|
|
68
|
+
// Explore, scout and advisor end with their objections to the brief, if any.
|
|
63
69
|
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.`;
|
|
64
70
|
const RESEARCH_PROMPT = `Research the assigned question. Read only the needed source; use web_search or web_fetch for external documentation. Answer with facts the main agent can act on: file:line, short quotes, what calls what, and what you checked and found nothing in. ${OBJECTIONS}`;
|
|
65
71
|
const SCOUT_PROMPT = `Scout the codebase for the assigned question. Search first (grep, find), then read only the parts that matter. Answer with facts the main agent can act on: file:line, short quotes, what calls what, and where you looked and found nothing. You have no web access: when the answer needs external documentation, say so. ${OBJECTIONS}`;
|
|
72
|
+
const ADVISOR_PROMPT = `Advise on the assigned decision. Read the project as needed. Give a recommendation, the options you rejected and why, and the risks, as points the main agent can act on (file:line where one rests on code). Say what you could not check. You have no web access. ${OBJECTIONS}`;
|
|
73
|
+
const GENERAL_PROMPT = `You are the general helper: you do the assigned work in your own clone of the repository, which is your working folder. Nothing you do there reaches the main agent's files. Make the change, run the checks that cover it, and COMMIT your finished work (git add -A, git commit): only committed work is returned. Do not rewrite or delete history before the starting commit. Answer with what you changed (file:line), the checks you ran and their result, and what you left undone. ${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 delegate.`;
|
|
66
74
|
export const READ_TOOLS = ['read', 'grep', 'find', 'ls'];
|
|
67
|
-
|
|
68
|
-
//
|
|
75
|
+
export const WRITE_TOOLS = [...READ_TOOLS, 'bash', 'edit', 'write'];
|
|
76
|
+
// What each helper may use. The project is read-only to every helper except
|
|
77
|
+
// general, which writes only its own clone (`writes`).
|
|
69
78
|
export const HELPER_SETUP = {
|
|
70
|
-
scout: { web: false, commands: false, prompt: SCOUT_PROMPT, label: '
|
|
71
|
-
research: { web: true, commands: false, prompt: RESEARCH_PROMPT, label: '
|
|
72
|
-
|
|
79
|
+
scout: { web: false, commands: false, prompt: SCOUT_PROMPT, label: 'Explore' },
|
|
80
|
+
research: { web: true, commands: false, prompt: RESEARCH_PROMPT, label: 'Scout' },
|
|
81
|
+
advisor: { web: false, commands: false, prompt: ADVISOR_PROMPT, label: 'Advisor' },
|
|
82
|
+
general: { web: false, commands: true, writes: true, prompt: GENERAL_PROMPT, label: 'General' },
|
|
73
83
|
review: { web: false, commands: false, prompt: REVIEW_PROMPT, label: 'Review' },
|
|
74
84
|
};
|
|
75
|
-
// A repository check a helper ran counts for the run like one the main
|
|
76
|
-
// agent ran, when the code was the same before and after it: the helper
|
|
77
|
-
// cannot write, but the main agent may edit beside it in the same answer.
|
|
78
|
-
export function countedCheck(managed, tool) {
|
|
79
|
-
if (tool.name !== 'bash') return tool;
|
|
80
|
-
return { ...tool, execute: async (...args) => {
|
|
81
|
-
// Whether anything in the folder moved during the check (another
|
|
82
|
-
// conversation's edit too: the suite ran on the whole folder); the check
|
|
83
|
-
// then counts for this conversation's own code.
|
|
84
|
-
const folder = async () => { try { return await (managed.folderDigest ?? managed.digest).call(managed); } catch { return null; } };
|
|
85
|
-
const counted = managed.run && isCheckCommand(args[1]?.command, managed.checks.commands), before = counted ? await folder() : null;
|
|
86
|
-
let ok = false;
|
|
87
|
-
try { const result = await tool.execute(...args); ok = !result?.isError; return result; }
|
|
88
|
-
finally {
|
|
89
|
-
const after = counted && before !== null ? await folder() : null;
|
|
90
|
-
if (after !== null && after === before) await managed.run.afterTool('bash', args[1], ok, () => managed.digest(), managed);
|
|
91
|
-
}
|
|
92
|
-
} };
|
|
93
|
-
}
|
|
@@ -4,19 +4,19 @@
|
|
|
4
4
|
// refusal, starts once more: Studio then gives the new helper the next model
|
|
5
5
|
// of its role that serves (an auto role's list).
|
|
6
6
|
import { fenced, HELPER_UNTRUSTED_RULE } from './untrusted.mjs';
|
|
7
|
-
import { relevantChecks } from './check-evidence.mjs';
|
|
8
7
|
import { jobTitle } from './call-purpose.mjs';
|
|
9
8
|
import { ManagedToolBoundary } from './tool-boundary.mjs';
|
|
10
9
|
import { managedResourceLoader } from './resource-loader.mjs';
|
|
11
10
|
import { shareGrant, releaseGrant, slotArgs, baseRole } from './grant-share.mjs';
|
|
12
11
|
import { describeFailure, failureCode, failureText } from '../runtime/managed-failure.mjs';
|
|
13
12
|
import { reviewParts, focusParts, mergeReviews, REVIEW_PARALLEL, emptySnapshot, readBranchBase, readPatchDigest, readPatchSnapshot, resolveReviewBase } from './patch-snapshot.mjs';
|
|
14
|
-
import { reviewFindings, briefIssues
|
|
13
|
+
import { reviewFindings, briefIssues } from './workflow.mjs';
|
|
15
14
|
import { currentWorkflow, helperWorkflowPrompt } from './workflow-context.mjs';
|
|
16
|
-
import { anchorText, relayObjection
|
|
15
|
+
import { anchorText, relayObjection } from './objections.mjs';
|
|
17
16
|
import { recordFindings } from './ledger.mjs';
|
|
18
17
|
import { copyNote, mergeCopies } from './copies.mjs';
|
|
19
|
-
import { HELPER_CALLS, HELPER_SETUP, READ_TOOLS,
|
|
18
|
+
import { HELPER_CALLS, HELPER_SETUP, READ_TOOLS, WRITE_TOOLS } from './helper-roles.mjs';
|
|
19
|
+
import { cloneProject, discardClone, takeBack, takeBackText, sweepText, recordGeneral } from './general-clone.mjs';
|
|
20
20
|
import { trackToolFailures } from './tool-failures.mjs';
|
|
21
21
|
import { helperMemoryTools, renewer } from './memory.mjs';
|
|
22
22
|
import { answeredSettlement, failedSettlement, settleHelper, OUTCOME_WORDS } from './helper-settlement.mjs';
|
|
@@ -88,7 +88,7 @@ async function helperAttempt(managed, role, task, signal, harness, snapshot, par
|
|
|
88
88
|
let results;
|
|
89
89
|
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; }))); }
|
|
90
90
|
catch (error) { throw signal?.aborted ? Error('managed-helper-cancelled') : error; }
|
|
91
|
-
const merged = mergeCopies(role, setup.label, results, { findingsOf: reviewFindings, issuesOf: briefIssues
|
|
91
|
+
const merged = mergeCopies(role, setup.label, results, { findingsOf: reviewFindings, issuesOf: briefIssues });
|
|
92
92
|
result = { ...results[0], reply: merged.reply, usage: merged.usage, review: merged.findings ? merged : null, stale: results.some(r => r.stale), report: null };
|
|
93
93
|
}
|
|
94
94
|
const { reply, usage, review, stale, from, grant } = result;
|
|
@@ -104,10 +104,9 @@ async function helperAttempt(managed, role, task, signal, harness, snapshot, par
|
|
|
104
104
|
if (!from) { managed.review = details; if (!stale) { recordFindings(managed, findings); managed.reviewFiles = await managed.changedFiles?.(); } }
|
|
105
105
|
managed.session.sessionManager.appendCustomEntry('agent-watch-review', details);
|
|
106
106
|
}
|
|
107
|
-
const verdicts = role === 'verify' ? recordVerdicts(managed, reply, harness, details) : '';
|
|
108
107
|
const report = result.report ?? answeredSettlement(role, reply, details, managed.helperSpecs?.get(role) ?? {}); details.outcome = report.outcome;
|
|
109
108
|
await managed.session.sendCustomMessage({ customType: 'agent-watch-helper-receipt', display: true,
|
|
110
|
-
content: `${setup.label}: ${usage.toLocaleString('en-US')} tokens${
|
|
109
|
+
content: `${setup.label}: ${usage.toLocaleString('en-US')} tokens${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 });
|
|
111
110
|
// An objection to the brief comes with the answer; a stale review has none to act on.
|
|
112
111
|
const objection = stale ? '' : await relayObjection(managed, role, reply, harness);
|
|
113
112
|
const ending = `Outcome: ${report.outcome}${report.outcome === 'unverifiable' ? ` (${report.summary})` : ''}.\n\n`;
|
|
@@ -137,7 +136,7 @@ async function answerUnder(managed, key, task, signal, harness, snapshot, parts,
|
|
|
137
136
|
try { verifyGrant(grant, managed.manifest, key); }
|
|
138
137
|
catch (error) { try { await managed.broker.request('close', slotArgs(key)); } catch { /* closes with the run */ } throw error; }
|
|
139
138
|
} catch (error) { throw Error(`managed-helper-failed: ${describeFailure(role, failureText(error))}`); }
|
|
140
|
-
let boundary, report = null;
|
|
139
|
+
let boundary, report = null, clone = null, liveRoot = null;
|
|
141
140
|
const sessions = new Set(), watched = new Set(), spec = managed.helperSpecs?.get(role) ?? {};
|
|
142
141
|
const renew = renewer(managed, key, g => verifyGrant(g, managed.manifest, key));
|
|
143
142
|
try {
|
|
@@ -146,20 +145,24 @@ async function answerUnder(managed, key, task, signal, harness, snapshot, parts,
|
|
|
146
145
|
// Studio's logs name the job by its title (the brief's first line without one).
|
|
147
146
|
(managed.jobTitles ??= new WeakMap()).set(runtime, jobTitle(task, title));
|
|
148
147
|
const setup = HELPER_SETUP[role], memory = helperMemoryTools(managed, () => grant, renew);
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
// General writes, but only in its own clone of the project (general-clone.mjs); the others read the project.
|
|
149
|
+
if (setup.writes) { clone = await cloneProject(managed); (managed.liveClones ??= new Set()).add(liveRoot = clone.root); recordGeneral(managed, clone, 'running'); }
|
|
150
|
+
const cwd = clone?.dir ?? managed.cwd;
|
|
151
|
+
boundary = new ManagedToolBoundary({ cwd, sdkRoot: managed.sdk, protectedRoots: managed.protectedRoots, readOnly: !setup.writes, commands: setup.commands, ...(clone ? { identity: managed.boundary.setup.identity, readRoots: [clone.objects] } : {}) });
|
|
152
|
+
// Always bypass would run a command on the host, outside the clone.
|
|
153
|
+
if (!setup.writes) boundary.unsandboxed = () => alwaysBypass(managed);
|
|
151
154
|
// One helper session answers the brief (with one part of the patch for a
|
|
152
155
|
// review in parts); its reply and tokens. A helper runs at its role's
|
|
153
156
|
// level from Studio (fixed by the Harness, or its main agent's).
|
|
154
157
|
const ask = async (extra, note = '', who = key) => {
|
|
155
|
-
const { session } = await managed.api.createAgentSession({ cwd
|
|
158
|
+
const { session } = await managed.api.createAgentSession({ cwd, model, modelRuntime: runtime, thinkingLevel: grant.effort || 'off',
|
|
156
159
|
settingsManager: managed.api.SettingsManager.inMemory({ retry: { enabled: false, provider: { maxRetries: 0 } }, cacheWarming: 'off' }),
|
|
157
|
-
sessionManager: managed.api.SessionManager.inMemory(
|
|
158
|
-
customTools: [...
|
|
160
|
+
sessionManager: managed.api.SessionManager.inMemory(cwd), noTools: 'builtin', tools: [...(setup.writes ? WRITE_TOOLS : READ_TOOLS), ...(setup.web ? ['web_search', 'web_fetch'] : []), ...memory.map(tool => tool.name)],
|
|
161
|
+
customTools: [...(setup.writes ? boundary.tools(managed.api).filter(tool => WRITE_TOOLS.includes(tool.name)) : unsandboxedBash(managed, boundary.tools(managed.api)).filter(tool => READ_TOOLS.includes(tool.name))),
|
|
159
162
|
...(setup.web ? managed.webTools(runtime, key) : []), ...memory], resourceLoader: managedResourceLoader(managed.api, { systemPrompt: setup.prompt + HELPER_UNTRUSTED_RULE }) });
|
|
160
163
|
sessions.add(session); trackToolFailures(managed, session, role); watched.add(who); watchHelper(managed, who, session, task, title);
|
|
161
164
|
const abort = () => void session.abort(); signal?.addEventListener('abort', abort, { once: true });
|
|
162
|
-
try { if (signal?.aborted) throw Error('managed-helper-cancelled'); await session.prompt(anchorText(managed.session.sessionManager, harness) + task + note + helperWorkflowPrompt(currentWorkflow(managed.session.sessionManager)) +
|
|
165
|
+
try { if (signal?.aborted) throw Error('managed-helper-cancelled'); await session.prompt(anchorText(managed.session.sessionManager, harness) + task + note + helperWorkflowPrompt(currentWorkflow(managed.session.sessionManager)) + extra, { expandPromptTemplates: false }); }
|
|
163
166
|
finally { signal?.removeEventListener('abort', abort); }
|
|
164
167
|
const messages = session.messages.filter(m => m.role === 'assistant');
|
|
165
168
|
// The main agent (and the member) learn why a helper failed, not just that it did.
|
|
@@ -191,9 +194,10 @@ async function answerUnder(managed, key, task, signal, harness, snapshot, parts,
|
|
|
191
194
|
let answers;
|
|
192
195
|
try { answers = await Promise.all(notes.map((note, i) => once(note, i ? `${role}#${i + 1}` : key))); }
|
|
193
196
|
catch (error) { for (const session of sessions) void session.abort(); throw error; }
|
|
194
|
-
const merged = mergeCopies(role, setup.label, answers, { findingsOf: reviewFindings, issuesOf: briefIssues
|
|
197
|
+
const merged = mergeCopies(role, setup.label, answers, { findingsOf: reviewFindings, issuesOf: briefIssues });
|
|
195
198
|
({ reply, usage } = merged); review = merged.findings ? merged : null;
|
|
196
199
|
}
|
|
200
|
+
if (clone) { const back = await takeBack(managed, clone); recordGeneral(managed, clone, 'returned', back); reply += takeBackText(clone, back) + sweepText(managed.orphans ?? { removed: [], unproven: [] }); managed.orphans = null; clone = null; }
|
|
197
201
|
const from = snapshot?.from ?? null;
|
|
198
202
|
const stale = snapshot ? (await (from ? readPatchDigest(managed.boundary, null, from.base) : managed.patchDigest())) !== snapshot.digest : false;
|
|
199
203
|
const findings = snapshot ? (review ? review.findings : reviewFindings(reply)) : null;
|
|
@@ -201,6 +205,8 @@ async function answerUnder(managed, key, task, signal, harness, snapshot, parts,
|
|
|
201
205
|
return { reply, usage, review, stale, from, grant, report };
|
|
202
206
|
} catch (error) { report = failedSettlement(role, error, signal, spec); throw error; }
|
|
203
207
|
finally {
|
|
208
|
+
if (clone) { discardClone(clone); recordGeneral(managed, clone, 'discarded'); }
|
|
209
|
+
managed.liveClones?.delete(liveRoot);
|
|
204
210
|
for (const session of sessions) session.dispose(); await boundary?.dispose();
|
|
205
211
|
await settleHelper(managed, renew.lease, report); // with the role's current token, before it closes
|
|
206
212
|
await closeRole(managed, key);
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// How a helper's attempt ended (Agent Studio 0.34+, POST /v1/managed/settle):
|
|
2
2
|
// one report per attempt, sent with the helper's own run token before its
|
|
3
3
|
// role closes. succeeded: the helper did its job and answered in the form its
|
|
4
|
-
// role needs (
|
|
4
|
+
// role needs (review's findings); failed: it could not
|
|
5
5
|
// (an error); cancelled: the member or the main agent stopped it;
|
|
6
6
|
// unverifiable: it answered without what its role needs, or its review went
|
|
7
7
|
// stale. Studio reads a helper role that closed without a report as
|
|
8
8
|
// unverifiable: absence is never success or failure.
|
|
9
9
|
import { CLIENT_AGENT } from './client-agent.mjs';
|
|
10
10
|
import { hasCapability } from './studio-capabilities.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import { ROLE_NAMES } from './helper-roles.mjs';
|
|
12
12
|
|
|
13
13
|
// The task as the main agent gives it: five optional fields beside the brief.
|
|
14
14
|
export const SPEC_FIELDS = Object.freeze({
|
|
@@ -20,7 +20,7 @@ export const SPEC_FIELDS = Object.freeze({
|
|
|
20
20
|
});
|
|
21
21
|
const field = description => ({ type: 'string', maxLength: 1000, description });
|
|
22
22
|
export function delegateParameters() {
|
|
23
|
-
return { type: 'object', properties: { role: { type: 'string', enum:
|
|
23
|
+
return { type: 'object', properties: { role: { type: 'string', enum: Object.keys(ROLE_NAMES) },
|
|
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)])),
|
|
@@ -37,7 +37,7 @@ export const SPEC_PROMPT = ' Give a helper target and acceptance at least (also
|
|
|
37
37
|
// brief; the main agent's task follows. The spec is kept for the settlement.
|
|
38
38
|
export function delegateArgs(self, args) {
|
|
39
39
|
const spec = Object.fromEntries(Object.keys(SPEC_FIELDS).map(k => [k, typeof args?.[k] === 'string' ? args[k].trim().slice(0, 1000) : '']).filter(([, v]) => v));
|
|
40
|
-
(self.helperSpecs ??= new Map()).set(args?.role, spec);
|
|
40
|
+
(self.helperSpecs ??= new Map()).set(ROLE_NAMES[args?.role], spec);
|
|
41
41
|
if (!Object.keys(spec).length) return args;
|
|
42
42
|
const head = Object.entries(spec).map(([k, v]) => `${k[0].toUpperCase()}${k.slice(1)}: ${v}`).join('\n');
|
|
43
43
|
return { ...args, task: `${head}\n\n${args.task}`.slice(0, 12000) };
|
|
@@ -58,18 +58,14 @@ export function answeredSettlement(role, reply, details = {}, spec = {}) {
|
|
|
58
58
|
const summary = clip(typeof parsed?.summary === 'string' && parsed.summary.trim() ? parsed.summary : prose || `${role} answered.`, 600);
|
|
59
59
|
const evidence = [], files = new Set();
|
|
60
60
|
let outcome = 'succeeded';
|
|
61
|
-
if (role === '
|
|
62
|
-
const verdicts = Array.isArray(parsed?.verdicts) ? parsed.verdicts : null;
|
|
63
|
-
if (!verdicts) outcome = 'unverifiable';
|
|
64
|
-
for (const v of verdicts ?? []) evidence.push(clip(`${v?.status ?? '?'}: ${v?.claim ?? ''} — ${v?.evidence ?? ''}`, 300));
|
|
65
|
-
} else if (role === 'review') {
|
|
61
|
+
if (role === 'review') {
|
|
66
62
|
if (details.stale) outcome = 'unverifiable';
|
|
67
63
|
else if (details.parsed === false) outcome = 'unverifiable';
|
|
68
64
|
for (const f of details.findings ?? []) { evidence.push(clip(`${f.severity} ${f.file ?? ''}:${f.line ?? ''} ${f.issue ?? ''}`, 300)); if (f.file) files.add(String(f.file)); }
|
|
69
65
|
}
|
|
70
66
|
for (const m of String(reply).matchAll(FILE_LINE)) {
|
|
71
67
|
files.add(m[1].replace(/^\.\//, ''));
|
|
72
|
-
if (role !== '
|
|
68
|
+
if (role !== 'review' && evidence.length < 8) evidence.push(clip(`${m[1]}:${m[2]}`, 300));
|
|
73
69
|
}
|
|
74
70
|
return { outcome, summary: details.stale ? 'The patch changed during review; this review is stale.' : summary,
|
|
75
71
|
evidence: [...new Set(evidence.filter(Boolean))].slice(0, 8), files: [...files].filter(f => f.length <= 300).slice(0, 50), ...(Object.keys(spec ?? {}).length ? { spec } : {}) };
|