@piagent/platform 1.15.0 → 1.15.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 +15 -0
- package/README.md +1 -1
- package/README.vi.md +1 -1
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/distribution-standard.md +6 -6
- 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-site/architecture.html +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/en/index.html +4 -4
- package/docs-site/content/en/whats-new.html +12 -0
- package/docs-site/content/index.html +4 -4
- package/docs-site/content/whats-new.html +12 -0
- 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 +5 -5
- 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/whats-new.html +13 -1
- package/docs-site/en/windows.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +5 -5
- 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/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/whats-new.html +13 -1
- 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/call-purpose.mjs +39 -0
- package/packages/piagent-core/managed/helper-roles.mjs +7 -3
- package/packages/piagent-core/managed/helper-run.mjs +6 -3
- package/packages/piagent-core/managed/patch-snapshot.mjs +29 -7
- package/packages/piagent-core/managed/request-stream.mjs +4 -1
- package/packages/piagent-core/managed/review-snapshot-worker.mjs +27 -3
- package/packages/piagent-core/managed/sandbox-diagnostic.mjs +4 -1
- package/packages/piagent-core/managed/session.mjs +40 -12
- package/packages/piagent-core/managed/web-search.mjs +1 -1
- package/packages/piagent-core/managed/workflow.mjs +19 -5
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-PIk4Femi.js → MarkdownMessage-DjA3OGyI.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{index-DeCt-FaN.js → index-bE2DKmc1.js} +4 -4
- package/packages/piagent-webui/dist/client/index.html +1 -1
- package/packages/piagent-webui/gateway/session-command-controller.ts +11 -0
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +2 -2
- package/packages/piagent-webui/ownership/session-lease-store.ts +20 -1
- package/scripts/piagent-cli.mjs +5 -1
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.15.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.15.1 docs · EN</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Language">
|
|
35
35
|
<a class="language-option" href="/windows" hreflang="vi" lang="vi">VI</a>
|
|
36
36
|
<a class="language-option current" href="/en/windows" hreflang="en" lang="en" aria-current="true">EN</a>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.15.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.15.1 docs · EN</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Language">
|
|
35
35
|
<a class="language-option" href="/workflows" hreflang="vi" lang="vi">VI</a>
|
|
36
36
|
<a class="language-option current" href="/en/workflows" hreflang="en" lang="en" aria-current="true">EN</a>
|
package/docs-site/index.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.15.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.15.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/" hreflang="en" lang="en">EN</a>
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
<main>
|
|
95
95
|
<div class="content-shell" data-article>
|
|
96
96
|
<section class="hero">
|
|
97
|
-
<p class="eyebrow">Piagent 1.15.
|
|
97
|
+
<p class="eyebrow">Piagent 1.15.1</p>
|
|
98
98
|
<h1>Coding agent cho cả team, kiểm tra từng bước.</h1>
|
|
99
99
|
<p>
|
|
100
100
|
Một dashboard cho Terminal và trình duyệt. Dùng model bằng tài khoản của bạn, hoặc model của công ty qua Agent Watch và Agent Studio. Guard kiểm tra từng tool call; ở chế độ công ty, Harness của team tự yêu cầu chạy check, cho subagent review và báo kết quả ngay dưới câu trả lời.
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
<div class="hero-actions">
|
|
109
109
|
<a class="button primary" href="/quickstart">Bắt đầu</a>
|
|
110
110
|
<a class="button" href="/ecosystem">Hệ sinh thái</a>
|
|
111
|
-
<a class="button" href="/whats-new">Có gì mới trong 1.15.
|
|
111
|
+
<a class="button" href="/whats-new">Có gì mới trong 1.15.1</a>
|
|
112
112
|
<a class="button" href="/commands">Xem command</a>
|
|
113
113
|
</div>
|
|
114
114
|
</section>
|
|
@@ -184,9 +184,9 @@ piagent-setup</code></pre>
|
|
|
184
184
|
<h2 id="bat-dau-tu-dau">Bắt đầu từ đâu</h2>
|
|
185
185
|
<div class="card-grid">
|
|
186
186
|
<a class="doc-card" href="/whats-new">
|
|
187
|
-
<span class="card-kicker">Bản 1.15.
|
|
187
|
+
<span class="card-kicker">Bản 1.15.1</span>
|
|
188
188
|
<h3>Có gì mới</h3>
|
|
189
|
-
<p>
|
|
189
|
+
<p>Studio thấy mỗi call của model dùng để làm gì, nhiều cuộc trò chuyện trong một folder không nhận nhầm thay đổi của nhau; ở 1.15.0: subagent bị từ chối nói đúng lý do; ở 1.14: cập nhật giữ model bạn đã chọn; ở 1.13: cuộc trò chuyện giữ chỗ đang đọc, chữ của agent có định dạng, Piagent trên Windows qua WSL2: sandbox công ty bằng bubblewrap và Agent Watch cho Windows; ở 1.12: @ chọn file và folder trên máy, skill và command dùng bằng /, nhánh cuộc trò chuyện công ty gửi được tin nhắn.</p>
|
|
190
190
|
</a>
|
|
191
191
|
<a class="doc-card" href="/ecosystem">
|
|
192
192
|
<span class="card-kicker">Get started</span>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.15.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.15.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/mcp-approval" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/mcp-approval" hreflang="en" lang="en">EN</a>
|
package/docs-site/mcp-auth.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.15.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.15.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/mcp-auth" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/mcp-auth" hreflang="en" lang="en">EN</a>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.15.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.15.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/mcp-servers" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/mcp-servers" hreflang="en" lang="en">EN</a>
|
package/docs-site/mcp.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.15.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.15.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/mcp" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/mcp" hreflang="en" lang="en">EN</a>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.15.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.15.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/permissions" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/permissions" hreflang="en" lang="en">EN</a>
|
package/docs-site/profiles.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.15.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.15.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/profiles" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/profiles" hreflang="en" lang="en">EN</a>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.15.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.15.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/quickstart" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/quickstart" hreflang="en" lang="en">EN</a>
|
package/docs-site/runtime.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.15.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.15.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/runtime" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/runtime" hreflang="en" lang="en">EN</a>
|
package/docs-site/scope.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.15.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.15.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/scope" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/scope" hreflang="en" lang="en">EN</a>
|
package/docs-site/security.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.15.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.15.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/security" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/security" hreflang="en" lang="en">EN</a>
|
package/docs-site/team.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.15.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.15.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/team" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/team" hreflang="en" lang="en">EN</a>
|
package/docs-site/whats-new.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.15.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.15.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/whats-new" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/whats-new" hreflang="en" lang="en">EN</a>
|
|
@@ -104,6 +104,18 @@
|
|
|
104
104
|
<h1>Bản 1.9.0: nói việc bằng lời, Harness của team kiểm tra</h1>
|
|
105
105
|
<p>Bỏ task contract và lệnh workflow. Chế độ công ty tự yêu cầu check và review; nút Dừng, lệnh có mạng theo lần duyệt và thẻ duyệt dễ đọc hơn.</p>
|
|
106
106
|
</div>
|
|
107
|
+
<section class="section">
|
|
108
|
+
<p class="section-kicker">v1.15.1 · 2026-10-07</p>
|
|
109
|
+
<h2 id="v1-15-1">Studio thấy mỗi call của model dùng để làm gì</h2>
|
|
110
|
+
<p class="section-lead">Mỗi call model trong phiên công ty báo cho Studio nó dùng để làm gì: trả lời tin nhắn, bước tiếp sau tool (kèm tên tool vừa chạy), lượt harness yêu cầu, tóm tắt hội thoại, subagent nhận việc (kèm tiêu đề một dòng của việc) hoặc tìm web. Không gửi thêm nội dung prompt nào khác. Studio 0.20.0 hiện chúng ở Logs → "Lượt agent".</p>
|
|
111
|
+
<ul>
|
|
112
|
+
<li>Nhiều cuộc trò chuyện công ty trong cùng một folder không còn nhận nhầm thay đổi của nhau: check, review và kế hoạch chỉ xét các file chính cuộc trò chuyện đó đã sửa.</li>
|
|
113
|
+
<li>Cuộc trò chuyện công ty mới không mở được runtime nói rõ lý do, và Lưu trữ hoặc Khởi động lại dùng được mà không cần khởi động lại dashboard.</li>
|
|
114
|
+
<li><span class="inline-code">mvnw</span>/<span class="inline-code">gradlew</span> tải bị chặn được chỉ sang <span class="inline-code">run_with_network</span>; <span class="inline-code">piagent doctor</span> chạy <span class="inline-code">piagent-doctor</span>.</li>
|
|
115
|
+
</ul>
|
|
116
|
+
<p>Cập nhật từ 1.14.x hoặc 1.15.0 không cần nhập lại cấu hình trong Agent Watch.</p>
|
|
117
|
+
</section>
|
|
118
|
+
|
|
107
119
|
<section class="section">
|
|
108
120
|
<p class="section-kicker">v1.15.0 · 2026-10-07</p>
|
|
109
121
|
<h2 id="v1-15-0">Subagent bị từ chối nói đúng lý do</h2>
|
package/docs-site/windows.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.15.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.15.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/windows" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/windows" hreflang="en" lang="en">EN</a>
|
package/docs-site/workflows.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.15.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.15.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/workflows" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/workflows" hreflang="en" lang="en">EN</a>
|
|
@@ -256,8 +256,8 @@
|
|
|
256
256
|
},
|
|
257
257
|
{
|
|
258
258
|
"path": "tests",
|
|
259
|
-
"fileCount":
|
|
260
|
-
"sha256": "
|
|
259
|
+
"fileCount": 524,
|
|
260
|
+
"sha256": "905a1c9999775af0f40d20589e23f047bb0ba5974b258cb5d7c2dcb255c56086"
|
|
261
261
|
}
|
|
262
262
|
],
|
|
263
263
|
"custodianComparisonRules": {
|
package/package.json
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// What a company model call is for, for Studio's logs (Logs → "Lượt agent"):
|
|
2
|
+
// the kind of call, the tools the answer before it called, and a subagent
|
|
3
|
+
// job's one-line title. No other prompt text leaves in these headers.
|
|
4
|
+
const SUMMARY_PROMPT = 'You are a context summarization assistant';
|
|
5
|
+
const TOOL = /^[a-z0-9_]{1,40}$/;
|
|
6
|
+
const TITLE_CHARS = 100;
|
|
7
|
+
|
|
8
|
+
// "read:3,bash": the tools of the assistant answer before this call, most
|
|
9
|
+
// used first, at most eight names (an unknown name counts as "other").
|
|
10
|
+
export function afterTools(message) {
|
|
11
|
+
const counts = new Map();
|
|
12
|
+
for (const part of message?.role === 'assistant' && Array.isArray(message.content) ? message.content : []) {
|
|
13
|
+
if (part?.type !== 'toolCall') continue;
|
|
14
|
+
const name = TOOL.test(String(part.name)) ? part.name : 'other';
|
|
15
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
16
|
+
}
|
|
17
|
+
return [...counts].sort((a, b) => b[1] - a[1]).slice(0, 8).map(([name, n]) => n > 1 ? `${name}:${Math.min(n, 9999)}` : name).join(',');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// A subagent job's title: the one the main agent gave, else the brief's
|
|
21
|
+
// first line; one line, at most TITLE_CHARS characters.
|
|
22
|
+
export function jobTitle(task, title) {
|
|
23
|
+
const line = [title, ...String(task ?? '').split('\n')].map(s => String(s ?? '').replace(/[\p{Cc}\s]+/gu, ' ').trim()).find(Boolean) ?? '';
|
|
24
|
+
return line.length > TITLE_CHARS ? line.slice(0, TITLE_CHARS - 1).trimEnd() + '…' : line;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The headers of one call. `owner.nextPurpose` is set by the harness when it
|
|
28
|
+
// sends the main agent back (checks, review findings, a missing plan).
|
|
29
|
+
export function purposeHeaders(owner, runtime, role, context) {
|
|
30
|
+
const messages = context?.messages ?? [], last = messages.at(-1);
|
|
31
|
+
let purpose, after = '';
|
|
32
|
+
if (typeof context?.systemPrompt === 'string' && context.systemPrompt.startsWith(SUMMARY_PROMPT)) purpose = 'summary';
|
|
33
|
+
else if (last?.role === 'toolResult') { purpose = 'tool_step'; after = afterTools(messages.findLast(m => m.role === 'assistant')); }
|
|
34
|
+
else if (role !== 'main') purpose = 'brief';
|
|
35
|
+
else if (owner.nextPurpose === 'harness') { purpose = 'harness'; owner.nextPurpose = null; }
|
|
36
|
+
else purpose = 'answer';
|
|
37
|
+
const title = purpose === 'brief' ? owner.jobTitles?.get(runtime) : '';
|
|
38
|
+
return { 'X-Agent-Purpose': purpose, ...(after ? { 'X-Agent-After': after } : {}), ...(title ? { 'X-Agent-Task': encodeURIComponent(title) } : {}) };
|
|
39
|
+
}
|
|
@@ -58,12 +58,16 @@ export const HELPER_SETUP = {
|
|
|
58
58
|
export function countedCheck(managed, tool) {
|
|
59
59
|
if (tool.name !== 'bash') return tool;
|
|
60
60
|
return { ...tool, execute: async (...args) => {
|
|
61
|
-
|
|
61
|
+
// Whether anything in the folder moved during the check (another
|
|
62
|
+
// conversation's edit too: the suite ran on the whole folder); the check
|
|
63
|
+
// then counts for this conversation's own code.
|
|
64
|
+
const folder = async () => { try { return await (managed.folderDigest ?? managed.digest).call(managed); } catch { return null; } };
|
|
65
|
+
const counted = managed.run && isCheckCommand(args[1]?.command, managed.checks.commands), before = counted ? await folder() : null;
|
|
62
66
|
let ok = false;
|
|
63
67
|
try { const result = await tool.execute(...args); ok = !result?.isError; return result; }
|
|
64
68
|
finally {
|
|
65
|
-
const after = counted && before !== null ? await
|
|
66
|
-
if (after !== null && after === before) await managed.run.afterTool('bash', args[1], ok,
|
|
69
|
+
const after = counted && before !== null ? await folder() : null;
|
|
70
|
+
if (after !== null && after === before) await managed.run.afterTool('bash', args[1], ok, () => managed.digest());
|
|
67
71
|
}
|
|
68
72
|
} };
|
|
69
73
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// A helper whose company model account is out of usage, or resting after a
|
|
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
|
+
import { jobTitle } from './call-purpose.mjs';
|
|
6
7
|
import { ManagedToolBoundary } from './tool-boundary.mjs';
|
|
7
8
|
import { managedResourceLoader } from './resource-loader.mjs';
|
|
8
9
|
import { shareGrant, releaseGrant } from './grant-share.mjs';
|
|
@@ -20,20 +21,20 @@ const textContent = result => result.content?.filter(p => p.type === 'text').map
|
|
|
20
21
|
const HELPER_SWITCH = new Set(['upstream_rate_limited', 'session_account_not_ready_retry_later', 'session_account_unavailable_start_new_session', 'account_capacity_unavailable', 'execution_capability_unavailable']);
|
|
21
22
|
const HELPER_ATTEMPTS = 2;
|
|
22
23
|
|
|
23
|
-
export async function runHelper(managed, role, task, signal, harness, { verifyGrant, patiently }) {
|
|
24
|
+
export async function runHelper(managed, role, task, signal, harness, { verifyGrant, patiently, title }) {
|
|
24
25
|
const snapshot = role === 'review' ? await managed.patchSnapshot() : null, parts = snapshot ? reviewParts(snapshot) : null;
|
|
25
26
|
// A helper whose company model account is out of usage, or resting after
|
|
26
27
|
// a refusal, starts once more: Studio then gives the new helper the next
|
|
27
28
|
// model of its role that serves (an auto role's list). Helpers read and
|
|
28
29
|
// run checks only, so starting over repeats no change.
|
|
29
30
|
for (let attempt = 1; ; attempt += 1) {
|
|
30
|
-
try { return await helperAttempt(managed, role, task, signal, harness, snapshot, parts, { verifyGrant, patiently }); }
|
|
31
|
+
try { return await helperAttempt(managed, role, task, signal, harness, snapshot, parts, { verifyGrant, patiently, title }); }
|
|
31
32
|
catch (error) {
|
|
32
33
|
if (attempt >= HELPER_ATTEMPTS || signal?.aborted || !HELPER_SWITCH.has(failureCode(error?.message)) || (managed.helperCalls.get(role) ?? 0) >= HELPER_CALLS) throw error;
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
|
-
async function helperAttempt(managed, role, task, signal, harness, snapshot, parts, { verifyGrant, patiently }) {
|
|
37
|
+
async function helperAttempt(managed, role, task, signal, harness, snapshot, parts, { verifyGrant, patiently, title }) {
|
|
37
38
|
// A long local tool step may outlive the main lease. Refresh authority
|
|
38
39
|
// before asking for a child; the broker serializes concurrent renewals,
|
|
39
40
|
// and main's own searches running beside it share managed renew.
|
|
@@ -54,6 +55,8 @@ async function helperAttempt(managed, role, task, signal, harness, snapshot, par
|
|
|
54
55
|
try {
|
|
55
56
|
const runtime = await managed.api.ModelRuntime.create({ credentials: new managed.ai.InMemoryCredentialStore(), modelsPath: null, refreshOnCreate: false, allowModelNetwork: false });
|
|
56
57
|
const model = managed.installModel(runtime, grant); managed.wrapStreams(runtime, role);
|
|
58
|
+
// Studio's logs name the job by its title (the brief's first line without one).
|
|
59
|
+
(managed.jobTitles ??= new WeakMap()).set(runtime, jobTitle(task, title));
|
|
57
60
|
const setup = HELPER_SETUP[role];
|
|
58
61
|
boundary = new ManagedToolBoundary({ cwd: managed.cwd, sdkRoot: managed.sdk, protectedRoots: managed.protectedRoots, readOnly: true, commands: setup.commands });
|
|
59
62
|
const checks = role === 'verify' ? `\n\nRepository checks: ${managed.checks.commands.length ? managed.checks.commands.map(c => '`' + c + '`').join(', ') : 'none declared or detected; choose the tests, type check or build that cover the claims'}.` : '';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { createHash } from 'node:crypto';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
6
6
|
const quote = value => "'" + value.replaceAll("'", "'\"'\"'") + "'";
|
|
@@ -9,8 +9,16 @@ const WORKER = fileURLToPath(new URL('./review-snapshot-worker.mjs', import.meta
|
|
|
9
9
|
const MAX_SNAPSHOT = 16 * 1024 * 1024;
|
|
10
10
|
|
|
11
11
|
// The worker runs inside the tool boundary; what it prints is one short line.
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// `scope`: only these paths (relative to the project) make the patch.
|
|
13
|
+
async function worker(boundary, mode, scope = null) {
|
|
14
|
+
let scopeName = null;
|
|
15
|
+
if (scope) {
|
|
16
|
+
scopeName = `scope-${randomUUID()}.json`;
|
|
17
|
+
fs.writeFileSync(path.join(boundary.temporary, scopeName), JSON.stringify([...scope]), { mode: 0o600, flag: 'wx' });
|
|
18
|
+
}
|
|
19
|
+
let result;
|
|
20
|
+
try { result = await boundary.invoke('bash', { command: `${quote(process.execPath)} ${quote(WORKER)} ${mode}${scopeName ? ' ' + scopeName : ''}`, timeout: 60 }); }
|
|
21
|
+
finally { if (scopeName) fs.rmSync(path.join(boundary.temporary, scopeName), { force: true }); }
|
|
14
22
|
const text = textContent(result).trim();
|
|
15
23
|
if (result.details?.truncation?.truncated || text.includes('\n')) throw Error(/review-(patch|file|untracked)-limit/.exec(text)?.[0] ?? 'managed-review-patch-unreadable');
|
|
16
24
|
const [digest, bytes, name] = text.split(' ');
|
|
@@ -20,15 +28,29 @@ async function worker(boundary, mode) {
|
|
|
20
28
|
|
|
21
29
|
// The digest of the patch a reviewer would see (diff against HEAD plus
|
|
22
30
|
// untracked files): whether a review is still current. Nothing else moves.
|
|
23
|
-
export async function readPatchDigest(boundary) { return (await worker(boundary, 'digest')).digest; }
|
|
31
|
+
export async function readPatchDigest(boundary, scope = null) { return (await worker(boundary, 'digest', scope)).digest; }
|
|
24
32
|
|
|
25
33
|
// The patch a reviewer sees, read inside the tool boundary. The worker writes
|
|
26
34
|
// it to the boundary's temporary directory and names it, with its digest, on
|
|
27
35
|
// its output; the file is read once, must match that digest (a file changed
|
|
28
36
|
// meanwhile never passes) and is removed.
|
|
29
|
-
export async function readPatchSnapshot(boundary) {
|
|
30
|
-
const { digest, bytes, name } = await worker(boundary, 'file');
|
|
37
|
+
export async function readPatchSnapshot(boundary, scope = null) {
|
|
38
|
+
const { digest, bytes, name } = await worker(boundary, 'file', scope);
|
|
31
39
|
if (!/^review-[0-9a-f-]{36}\.json$/.test(name ?? '')) throw Error('managed-review-patch-unreadable');
|
|
40
|
+
return { patch: readOnce(boundary, name, bytes, digest).toString('utf8'), digest };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// What each changed or new file of the project holds now: { path: hash }.
|
|
44
|
+
// Compared before and after a command, it names the files that command
|
|
45
|
+
// changed, so a conversation answers for its own changes, not for another
|
|
46
|
+
// conversation's in the same folder.
|
|
47
|
+
export async function readChangedFiles(boundary) {
|
|
48
|
+
const { digest, bytes, name } = await worker(boundary, 'files');
|
|
49
|
+
if (!/^files-[0-9a-f-]{36}\.json$/.test(name ?? '')) throw Error('managed-review-patch-unreadable');
|
|
50
|
+
return new Map(Object.entries(JSON.parse(readOnce(boundary, name, bytes, digest).toString('utf8'))));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readOnce(boundary, name, bytes, digest) {
|
|
32
54
|
const file = path.join(boundary.temporary, name);
|
|
33
55
|
let data;
|
|
34
56
|
try {
|
|
@@ -40,7 +62,7 @@ export async function readPatchSnapshot(boundary) {
|
|
|
40
62
|
} finally { fs.closeSync(fd); }
|
|
41
63
|
} finally { fs.rmSync(file, { force: true }); }
|
|
42
64
|
if (createHash('sha256').update(data).digest('hex') !== digest) throw Error('managed-review-patch-changed');
|
|
43
|
-
return
|
|
65
|
+
return data;
|
|
44
66
|
}
|
|
45
67
|
|
|
46
68
|
// Why a required review could not run, in one word for the member.
|
|
@@ -9,6 +9,7 @@ import { setTimeout as sleep } from 'node:timers/promises';
|
|
|
9
9
|
import { describeFailure, failureCode, failureIsAdmissionRefusal, failureIsBriefRefusal, failureReason, unnamedFailureText } from '../runtime/managed-failure.mjs';
|
|
10
10
|
import { CLIENT_AGENT } from './client-agent.mjs';
|
|
11
11
|
import { shareGrant, releaseGrant } from './grant-share.mjs';
|
|
12
|
+
import { purposeHeaders } from './call-purpose.mjs';
|
|
12
13
|
|
|
13
14
|
// How long one request waits in line for a free company model account.
|
|
14
15
|
const CAPACITY_WAIT_MS = 20 * 60_000;
|
|
@@ -25,6 +26,8 @@ export function wrapRoleStreams(owner, runtime, role, { provider, verifyGrant })
|
|
|
25
26
|
runtime[method] = (model, context, options) => {
|
|
26
27
|
// Tools run between two answers: the count names the answer they came from.
|
|
27
28
|
if (role === 'main') owner.mainAnswers = (owner.mainAnswers ?? 0) + 1;
|
|
29
|
+
// What this call is for, named once: an ask again after a refusal is the same call.
|
|
30
|
+
const purpose = purposeHeaders(owner, runtime, role, context);
|
|
28
31
|
const out = owner.ai.createAssistantMessageEventStream(), signal = options?.signal;
|
|
29
32
|
void (async () => {
|
|
30
33
|
let wait = null, noted = false, brief = 0, refused = null, rebinds = 0, share = null;
|
|
@@ -59,7 +62,7 @@ export function wrapRoleStreams(owner, runtime, role, { provider, verifyGrant })
|
|
|
59
62
|
// same company account and prompt cache; a helper names its role.
|
|
60
63
|
const conversation = role === 'main' ? conversationSession(owner) : null;
|
|
61
64
|
const stream = original({ ...model, id: route.native }, context, { ...options, apiKey: grant.token, sessionId: conversation ?? grant.role_id, maxRetries: 0,
|
|
62
|
-
headers: { ...options?.headers, 'User-Agent': CLIENT_AGENT, 'X-Session-Id': grant.role_id, ...(conversation ? { 'X-Claude-Code-Session-Id': conversation } : {}) } });
|
|
65
|
+
headers: { ...options?.headers, ...purpose, 'User-Agent': CLIENT_AGENT, 'X-Session-Id': grant.role_id, ...(conversation ? { 'X-Claude-Code-Session-Id': conversation } : {}) } });
|
|
63
66
|
// Whether Studio admitted the request is known at its first event
|
|
64
67
|
// after "start": until then nothing is passed on, so a refused
|
|
65
68
|
// request leaves no trace in the conversation.
|
|
@@ -12,8 +12,33 @@ import { managedGit } from './toolchain.mjs';
|
|
|
12
12
|
const MAX_SNAPSHOT = 16 * 1024 * 1024, MAX_UNTRACKED = 2000, READ_FILE = 400_000, BINARY_INLINE = 120_000, HASHED_FILE = 256 * 1024 * 1024;
|
|
13
13
|
const git = args => execFileSync(managedGit(), ['--no-pager', ...args], { encoding: 'utf8', maxBuffer: MAX_SNAPSHOT + 1, env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_CONFIG_NOSYSTEM: '1' } });
|
|
14
14
|
const sha256 = data => createHash('sha256').update(data).digest('hex');
|
|
15
|
-
const
|
|
16
|
-
|
|
15
|
+
const mode = process.argv[2];
|
|
16
|
+
// "files": what each changed or new file holds now (a hash, "deleted"), so
|
|
17
|
+
// the runtime can tell which files one conversation's command changed.
|
|
18
|
+
if (mode === 'files') {
|
|
19
|
+
const changed = git(['diff', '--name-only', '--no-renames', '--relative', '-z', 'HEAD', '--', '.']).split('\0').filter(Boolean);
|
|
20
|
+
const fresh = git(['ls-files', '--others', '--exclude-standard', '-z']).split('\0').filter(Boolean);
|
|
21
|
+
if (changed.length + fresh.length > MAX_UNTRACKED * 4) throw Error('review-untracked-limit');
|
|
22
|
+
const print = name => {
|
|
23
|
+
let stat; try { stat = fs.lstatSync(name); } catch { return 'deleted'; }
|
|
24
|
+
if (stat.isSymbolicLink()) return 'link:' + sha256(fs.readlinkSync(name));
|
|
25
|
+
if (stat.isDirectory()) return 'dir:' + sha256(fs.readdirSync(name).sort().join('\0'));
|
|
26
|
+
return stat.size > HASHED_FILE ? `size:${stat.size}:${stat.mtimeMs}` : sha256(fs.readFileSync(name)) + ':' + (stat.mode & 0o777);
|
|
27
|
+
};
|
|
28
|
+
const files = Object.fromEntries([...new Set([...changed, ...fresh])].sort().map(name => [name, print(name)]));
|
|
29
|
+
const name = `files-${randomUUID()}.json`, data = Buffer.from(JSON.stringify(files));
|
|
30
|
+
fs.writeFileSync(path.join(os.tmpdir(), name), data, { mode: 0o600, flag: 'wx' });
|
|
31
|
+
process.stdout.write(`${sha256(data)} ${data.length} ${name}`);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
// A scope (a file the runtime wrote in this boundary's temporary directory):
|
|
35
|
+
// only these paths, the ones this conversation changed, make the patch.
|
|
36
|
+
const scopeName = process.argv[3];
|
|
37
|
+
if (scopeName !== undefined && !/^scope-[0-9a-f-]{36}\.json$/.test(scopeName)) throw Error('review-scope-invalid');
|
|
38
|
+
const scope = scopeName ? new Set(JSON.parse(fs.readFileSync(path.join(os.tmpdir(), scopeName), 'utf8'))) : null;
|
|
39
|
+
if (scope && [...scope].some(p => typeof p !== 'string' || !p || path.isAbsolute(p) || p.split('/').includes('..'))) throw Error('review-scope-invalid');
|
|
40
|
+
const patch = scope && !scope.size ? '' : git(['diff', '--binary', '--no-ext-diff', '--no-textconv', '--no-renames', 'HEAD', '--', ...(scope ? [...scope].sort().map(p => ':(literal)' + p) : ['.'])]);
|
|
41
|
+
const paths = git(['ls-files', '--others', '--exclude-standard', '-z']).split('\0').filter(Boolean).filter(p => !scope || scope.has(p)).sort();
|
|
17
42
|
if (paths.length > MAX_UNTRACKED) throw Error('review-untracked-limit');
|
|
18
43
|
const untracked = paths.map(name => {
|
|
19
44
|
const stat = fs.lstatSync(name);
|
|
@@ -36,7 +61,6 @@ if (snapshot.length > MAX_SNAPSHOT) throw Error('review-patch-limit');
|
|
|
36
61
|
// the snapshot goes to a new file in this boundary's temporary directory,
|
|
37
62
|
// named on stdout with its digest; the runtime reads it once, checks the
|
|
38
63
|
// digest and removes it. No argument: the snapshot itself.
|
|
39
|
-
const mode = process.argv[2];
|
|
40
64
|
if (mode === 'digest') process.stdout.write(`${sha256(snapshot)} ${snapshot.length}`);
|
|
41
65
|
else if (mode === 'file') {
|
|
42
66
|
const name = `review-${randomUUID()}.json`;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const DENIAL = /\b(?:EPERM|EACCES)\b|operation not permitted|permission denied/i;
|
|
2
|
-
|
|
2
|
+
// A download that never left the machine: resolvers, npm/pip, and the curl or
|
|
3
|
+
// wget a build wrapper runs (mvnw/gradlew fetch their distribution: "curl:
|
|
4
|
+
// Failed to fetch https://repo.maven.apache.org/…").
|
|
5
|
+
const OFFLINE = /ENOTFOUND|EAI_AGAIN|Could not resolve host|getaddrinfo|npm error network|Failed to establish a new connection|\bcurl: (?:\(\d+\) )?Failed to (?:fetch|connect)|\bwget: unable to resolve|Unknown host|UnknownHostException/i;
|
|
3
6
|
// `network`: the member approved this command and it ran with network, so a
|
|
4
7
|
// name that does not resolve is the machine's network, not the sandbox.
|
|
5
8
|
// `isolated`: a proxy listens on this Mac, so without approval a command does
|