@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
|
@@ -121,10 +121,14 @@ function linkGradleDistributions(source, target) {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
// The per-project package cache a sandboxed command may write.
|
|
125
|
+
export function cacheDirectory(userHome, repositoryTop) {
|
|
126
|
+
const root = process.platform === 'linux' ? path.join(process.env.XDG_CACHE_HOME || path.join(userHome, '.cache'), 'piagent/sandbox') : path.join(userHome, 'Library/Caches/Piagent/sandbox');
|
|
127
|
+
return path.join(root, createHash('sha256').update(repositoryTop).digest('hex').slice(0, 24));
|
|
128
|
+
}
|
|
124
129
|
export function languageEnvironment({ userHome, repositoryTop, home }) {
|
|
125
130
|
const linux = process.platform === 'linux';
|
|
126
|
-
const
|
|
127
|
-
const cache = path.join(root, createHash('sha256').update(repositoryTop).digest('hex').slice(0, 24));
|
|
131
|
+
const cache = cacheDirectory(userHome, repositoryTop), root = path.dirname(cache);
|
|
128
132
|
fs.mkdirSync(cache, { recursive: true, mode: 0o700 });
|
|
129
133
|
fs.chmodSync(cache, 0o700);
|
|
130
134
|
const now = new Date(); try { fs.utimesSync(cache, now, now); } catch { /* best effort */ }
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { callName } from './role-names.mjs';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { briefIssues } from './workflow.mjs';
|
|
4
5
|
|
|
5
6
|
// The objection ledger: what a helper holds against the main agent's work in
|
|
6
7
|
// one run (one member message), and what the main agent decided about it.
|
|
7
|
-
// A helper's objection to its brief
|
|
8
|
-
//
|
|
8
|
+
// A helper's objection to its brief and a review finding are one kind
|
|
9
|
+
// of item: open until the main agent resolves it with
|
|
9
10
|
// resolve_objection (accepted with what it changed, or rejected with what it
|
|
10
11
|
// checked and why), and a rejection of a required item goes back to the
|
|
11
12
|
// helper once; a disagreement that remains goes to the member. The run's
|
|
@@ -50,7 +51,7 @@ const open = run => ledgerOf(run).filter(i => i.status === 'open');
|
|
|
50
51
|
export const blockingItems = run => open(run).filter(i => i.required && i.source === 'brief');
|
|
51
52
|
|
|
52
53
|
// Adds items from one helper answer. `source`: brief (an objection to the
|
|
53
|
-
// brief), review (blocking and major findings)
|
|
54
|
+
// brief), review (blocking and major findings)
|
|
54
55
|
export function addItems(managed, source, role, list, { required } = {}) {
|
|
55
56
|
const run = managed.run;
|
|
56
57
|
if (!run || !list?.length) return [];
|
|
@@ -64,7 +65,7 @@ export function addItems(managed, source, role, list, { required } = {}) {
|
|
|
64
65
|
}
|
|
65
66
|
return added;
|
|
66
67
|
}
|
|
67
|
-
export const itemText = i => `${i.id} [${i.source === 'brief' ? i.kind : i.source === 'review' ? i.kind : 'failed claim'}] ${i.role}: ${i.detail}${i.evidence ? ` (evidence: ${i.evidence}${i.place === 'missing' ? '; the harness found no such place' : ''})` : ''}`;
|
|
68
|
+
export const itemText = i => `${i.id} [${i.source === 'brief' ? i.kind : i.source === 'review' ? i.kind : 'failed claim'}] ${callName(i.role)}: ${i.detail}${i.evidence ? ` (evidence: ${i.evidence}${i.place === 'missing' ? '; the harness found no such place' : ''})` : ''}`;
|
|
68
69
|
export const setStatus = (item, status, extra = {}) => Object.assign(item, { status, ...extra });
|
|
69
70
|
export function saveLedger(managed) {
|
|
70
71
|
const items = ledgerOf(managed.run);
|
|
@@ -75,7 +76,7 @@ export function saveLedger(managed) {
|
|
|
75
76
|
// checked. An accepted item names what changed; a rejected one says why.
|
|
76
77
|
export function resolveTool(owner) {
|
|
77
78
|
return { name: 'resolve_objection', label: 'Objection',
|
|
78
|
-
description: 'Record your decision on an objection a helper raised (its id, such as o1): a brief objection
|
|
79
|
+
description: 'Record your decision on an objection a helper raised (its id, such as o1): a brief objection or a review finding. Check it first at the place its evidence names. accepted: you agree; say what you changed (a corrected brief, the plan, the code). rejected: say what you checked (file:line, a command and its result) and why the objection does not hold; a rejected required objection goes back to the helper, and a disagreement that remains goes to the member.',
|
|
79
80
|
parameters: { type: 'object', properties: { id: { type: 'string', maxLength: 10 }, decision: { type: 'string', enum: DECISIONS }, checked: { type: 'string', minLength: 1, maxLength: 300 },
|
|
80
81
|
change: { type: 'string', maxLength: 300 }, reason: { type: 'string', maxLength: 500 } }, required: ['id', 'decision', 'checked'], additionalProperties: false },
|
|
81
82
|
execute: async (_id, args) => {
|
|
@@ -87,7 +88,7 @@ export function resolveTool(owner) {
|
|
|
87
88
|
if (args.decision === 'rejected' && !clip(args.reason, 500)) throw Error('managed-objection-invalid: a rejected objection says why it does not hold.');
|
|
88
89
|
const resolution = { decision: args.decision, checked: clip(args.checked, 300), ...(args.change ? { change: clip(args.change, 300) } : {}), ...(args.reason ? { reason: clip(args.reason, 500) } : {}) };
|
|
89
90
|
setStatus(item, args.decision, { resolution });
|
|
90
|
-
const back = args.decision === 'rejected' && item.required ? ` The ${item.role} subagent judges your reason before the turn ends.` : '';
|
|
91
|
+
const back = args.decision === 'rejected' && item.required ? ` The ${callName(item.role)} subagent judges your reason before the turn ends.` : '';
|
|
91
92
|
return { content: [{ type: 'text', text: `Recorded: ${item.id} ${args.decision}.${back}` }], details: { id: item.id, decision: args.decision } };
|
|
92
93
|
} };
|
|
93
94
|
}
|
|
@@ -125,12 +126,12 @@ export async function settleLedger(managed, run, { note, send, signal }) {
|
|
|
125
126
|
const rejected = toRejudge(run)[0];
|
|
126
127
|
if (rejected) {
|
|
127
128
|
rejected.rejudged = true; let reply;
|
|
128
|
-
await note(`Harness: the ${rejected.role} subagent judges the main agent's reason for rejecting ${rejected.id}.`, { phase: 'rejudge', role: rejected.role, id: rejected.id });
|
|
129
|
-
try { reply = (await managed.delegate({ role: rejected.role, task: rejudgeBrief(rejected) }, signal, { harness: true })).content.map(p => p.text).join('\n'); }
|
|
129
|
+
await note(`Harness: the ${callName(rejected.role)} subagent judges the main agent's reason for rejecting ${rejected.id}.`, { phase: 'rejudge', role: rejected.role, id: rejected.id });
|
|
130
|
+
try { reply = (await managed.delegate({ role: callName(rejected.role), task: rejudgeBrief(rejected) }, signal, { harness: true })).content.map(p => p.text).join('\n'); }
|
|
130
131
|
catch { return true; }
|
|
131
132
|
if (!briefIssues(reply)?.length) { setStatus(rejected, 'withdrawn'); return true; }
|
|
132
133
|
setStatus(rejected, 'disputed'); run.disputes.push({ source: rejected.role, brief: true });
|
|
133
|
-
await note(`Harness: the ${rejected.role} subagent keeps its objection; the member decides.\n${itemText(rejected)}`, { phase: 'dispute', role: rejected.role, id: rejected.id });
|
|
134
|
+
await note(`Harness: the ${callName(rejected.role)} subagent keeps its objection; the member decides.\n${itemText(rejected)}`, { phase: 'dispute', role: rejected.role, id: rejected.id });
|
|
134
135
|
return true;
|
|
135
136
|
}
|
|
136
137
|
const asked = run.decidedAsked ??= new Set(), pending = undecided(run).filter(item => !asked.has(item.id));
|
|
@@ -142,7 +143,7 @@ export async function settleLedger(managed, run, { note, send, signal }) {
|
|
|
142
143
|
|
|
143
144
|
// What the main agent still has to decide before the turn ends: required
|
|
144
145
|
// items, review findings above minor, brief objections it never answered.
|
|
145
|
-
export const undecided = run => open(run)
|
|
146
|
+
export const undecided = run => open(run);
|
|
146
147
|
export function decideText(items) {
|
|
147
148
|
return `Harness: ${items.length} objection(s) have no decision yet:\n${items.map(i => '- ' + itemText(i)).join('\n')}\nCheck each at the place its evidence names, then record your decision with resolve_objection: accepted with what you changed, or rejected with what you checked and why. Minor review findings need nothing; no new review follows.`;
|
|
148
149
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { callName } from './helper-roles.mjs';
|
|
2
|
+
import { briefIssues, currentPlan, planText } from './workflow.mjs';
|
|
2
3
|
import { addItems, itemText, ledgerOf, setStatus } from './ledger.mjs';
|
|
3
4
|
export { resolveTool } from './ledger.mjs';
|
|
4
5
|
|
|
@@ -42,10 +43,10 @@ export function anchorText(manager, harness) {
|
|
|
42
43
|
// reviews the final patch), else once, and again only after blocking findings.
|
|
43
44
|
export function beforeDelegate(run, role, harness, { since = null, review = null, changed = false, asked = false } = {}) {
|
|
44
45
|
if (!run || harness) return;
|
|
45
|
-
if (run.disputes.some(d => d.source === role && d.brief)) throw Error(`managed-helper-disputed: the ${role} subagent objected to your briefs twice for this message, so the member decides. Tell them what it objects to and what you decided; do not delegate to it again before their next message.`);
|
|
46
|
+
if (run.disputes.some(d => d.source === role && d.brief)) throw Error(`managed-helper-disputed: the ${callName(role)} subagent objected to your briefs twice for this message, so the member decides. Tell them what it objects to and what you decided; do not delegate to it again before their next message.`);
|
|
46
47
|
const mine = ledgerOf(run).filter(i => i.source === 'brief' && i.role === role && i.status === 'open');
|
|
47
48
|
const blocked = mine.filter(i => i.required);
|
|
48
|
-
if (blocked.length) throw Error(`managed-objection-open: the ${role} subagent objected to your brief; record your decision with resolve_objection before you brief it again.\n${blocked.map(itemText).join('\n')}`);
|
|
49
|
+
if (blocked.length) throw Error(`managed-objection-open: the ${callName(role)} subagent objected to your brief; record your decision with resolve_objection before you brief it again.\n${blocked.map(itemText).join('\n')}`);
|
|
49
50
|
// Reviewers the member asked for (copies.mjs) run at their word.
|
|
50
51
|
if (role === 'review' && !since && changed && !asked) {
|
|
51
52
|
if (run.policy.review === 'require') throw Error('managed-review-by-harness: the harness reviews the final patch when you finish; do not ask for a review yourself. Finish the change and its checks.');
|
|
@@ -67,29 +68,11 @@ export async function relayObjection(managed, role, reply, harness) {
|
|
|
67
68
|
const second = ledgerOf(run).some(i => i.source === 'brief' && i.role === role), items = addItems(managed, 'brief', role, issues), list = items.map(itemText).join('\n');
|
|
68
69
|
if (second) { run.disputes.push({ source: role, brief: true }); for (const item of items) setStatus(item, 'disputed'); }
|
|
69
70
|
await managed.session.sendCustomMessage({ customType: 'agent-watch-process', display: true,
|
|
70
|
-
content: second ? `Harness: the ${role} subagent objected to a brief a second time for this message; the exchange stops and the member decides.\n${list}`
|
|
71
|
-
: `Harness: the ${role} subagent objected to the brief the main agent gave it:\n${list}`,
|
|
71
|
+
content: second ? `Harness: the ${callName(role)} subagent objected to a brief a second time for this message; the exchange stops and the member decides.\n${list}`
|
|
72
|
+
: `Harness: the ${callName(role)} subagent objected to the brief the main agent gave it:\n${list}`,
|
|
72
73
|
details: { phase: second ? 'dispute' : 'objection', role, by: 'main', issues: issues.map(({ kind, detail }) => ({ kind, detail })), ids: items.map(i => i.id) } }, { triggerTurn: false });
|
|
73
|
-
if (second) return `\n\n[Harness] The ${role} subagent objected to your brief again. This is its second objection for this message, so the harness ends the exchange: do not delegate to it again before the member's next message. Tell the member plainly what it objects to and what you decided, and let them decide.`;
|
|
74
|
+
if (second) return `\n\n[Harness] The ${callName(role)} subagent objected to your brief again. This is its second objection for this message, so the harness ends the exchange: do not delegate to it again before the member's next message. Tell the member plainly what it objects to and what you decided, and let them decide.`;
|
|
74
75
|
const blocking = items.filter(i => i.required);
|
|
75
|
-
return `\n\n[Harness] The ${role} subagent objected to your brief:\n${list}\nCheck each at the place its evidence names, then record your decision with resolve_objection (accepted with what you changed, or rejected with what you checked and why).${blocking.length ? ` Until you do, file changes are refused: ${blocking.map(i => i.id).join(', ')} say${blocking.length === 1 ? 's' : ''} the brief rests on a wrong premise or conflicts with the request.` : ''} A rejected objection goes back to the helper; a second objection from it for this message goes to the member.`;
|
|
76
|
+
return `\n\n[Harness] The ${callName(role)} subagent objected to your brief:\n${list}\nCheck each at the place its evidence names, then record your decision with resolve_objection (accepted with what you changed, or rejected with what you checked and why).${blocking.length ? ` Until you do, file changes are refused: ${blocking.map(i => i.id).join(', ')} say${blocking.length === 1 ? 's' : ''} the brief rests on a wrong premise or conflicts with the request.` : ''} A rejected objection goes back to the helper; a second objection from it for this message goes to the member.`;
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
// Verify's verdicts, counted for the receipt; its failed claims go into the
|
|
79
|
-
// ledger (required under a required verify, noted otherwise). Under a
|
|
80
|
-
// required verify the failed claims are the run's to settle; once the agent
|
|
81
|
-
// has been sent back with them, only the harness's own re-check replaces
|
|
82
|
-
// them, and a claim it no longer fails is cleared.
|
|
83
|
-
export function recordVerdicts(managed, reply, harness, details) {
|
|
84
|
-
const run = managed.run, found = verifyVerdicts(reply);
|
|
85
|
-
details.verdicts = found ? Object.fromEntries(['pass', 'fail', 'unverifiable'].map(s => [s, found.filter(v => v.status === s).length])) : null;
|
|
86
|
-
if (run && found && (harness || !run.claimsAsked)) {
|
|
87
|
-
run.verifyFails = found.filter(v => v.status === 'fail');
|
|
88
|
-
const failed = new Set(run.verifyFails.map(v => v.claim)), required = run.policy.verify === 'require';
|
|
89
|
-
for (const item of ledgerOf(run)) if (item.source === 'verify' && ['open', 'rejected', 'noted'].includes(item.status) && !failed.has(item.detail)) setStatus(item, 'cleared');
|
|
90
|
-
const known = new Set(ledgerOf(run).filter(i => i.source === 'verify' && i.status !== 'cleared').map(i => i.detail));
|
|
91
|
-
const added = addItems(managed, 'verify', 'verify', run.verifyFails.filter(v => !known.has(v.claim)).map(v => ({ kind: 'fail', detail: v.claim, evidence: v.evidence })), { required });
|
|
92
|
-
if (!required) for (const item of added) setStatus(item, 'noted');
|
|
93
|
-
}
|
|
94
|
-
return found ? ' · ' + Object.entries(details.verdicts).filter(([, n]) => n).map(([s, n]) => `${n} ${s}`).join(', ') : '';
|
|
95
|
-
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// A helper's working name (what the main agent calls it) and its Harness role
|
|
2
|
+
// (Studio's grant). No imports: ledger.mjs and copies.mjs need these names
|
|
3
|
+
// without loading helper-roles.mjs, which imports workflow.mjs (a cycle).
|
|
4
|
+
export const ROLE_NAMES = Object.freeze({ explore: 'scout', scout: 'research', advisor: 'advisor', general: 'general', review: 'review' });
|
|
5
|
+
export const HELPER_ROLES = Object.freeze(Object.values(ROLE_NAMES));
|
|
6
|
+
export const callName = role => Object.keys(ROLE_NAMES).find(name => ROLE_NAMES[name] === role) ?? role;
|
|
@@ -14,18 +14,15 @@ import { nativeManagedModel, isVendor, piProvider, studioAPI, studioPath } from
|
|
|
14
14
|
import { fetchOriginTool } from './repository-operation.mjs';
|
|
15
15
|
import { describeFailure, failureCode, failureText } from '../runtime/managed-failure.mjs';
|
|
16
16
|
import { enableCompanyTurnRetry, wrapRoleStreams, conversationSession } from './request-stream.mjs'; import { StudioLink, closeRun } from './studio-link.mjs';
|
|
17
|
-
import { trackToolFailures } from './tool-failures.mjs';
|
|
18
|
-
import {
|
|
19
|
-
import { runHelper } from './helper-run.mjs';
|
|
20
|
-
import { askTool } from './member-questions.mjs';
|
|
21
|
-
import { setTimeout as sleep } from 'node:timers/promises';
|
|
17
|
+
import { trackToolFailures } from './tool-failures.mjs'; import { fastServed, fastSetter, fastState, recordFast, restoreFast } from './fast-mode.mjs';
|
|
18
|
+
import { runHelper } from './helper-run.mjs'; import { sweepGeneral } from './general-clone.mjs'; import { askTool } from './member-questions.mjs';
|
|
22
19
|
import { stageCompaction, compactEarly, backOffFailedCompaction, trackCompaction, boundRequestImages } from './compaction.mjs';
|
|
23
20
|
import { readPatchDigest, readPatchSnapshot, readChangedFiles, readHead, readRepositoryHeads } from './patch-snapshot.mjs';
|
|
24
|
-
import { memberTurn } from './continuation.mjs';
|
|
21
|
+
import { memberTurn } from './continuation.mjs'; import { setTimeout as sleep } from 'node:timers/promises';
|
|
25
22
|
import { workflowPolicy, repositoryChecks, sessionPlanTool, currentPlan, workflowPrompt, RunProcess, completionGate, pendingBaseline, pendingPaths, pendingHead, processEditTools } from './workflow.mjs';
|
|
26
23
|
import { workflowContextTool, workflowContextExtension } from './workflow-context.mjs';
|
|
27
24
|
import { beforeDelegate, resolveTool } from './objections.mjs'; import { fanOut, withDroppedNote } from './copies.mjs'; import { withMemberMcp } from './member-mcp.mjs';
|
|
28
|
-
import { HELPER_CALLS, HELPER_ROLES, helperRoles, helperPrompt, webPrompt, delegateDescription, mainWebLookups, limitedWebTools } from './helper-roles.mjs'; import { delegateParameters, delegateArgs, SPEC_PROMPT } from './helper-settlement.mjs';
|
|
25
|
+
import { HELPER_CALLS, HELPER_ROLES, ROLE_NAMES, callName, helperRoles, helperPrompt, webPrompt, delegateDescription, mainWebLookups, limitedWebTools } from './helper-roles.mjs'; import { delegateParameters, delegateArgs, SPEC_PROMPT } from './helper-settlement.mjs';
|
|
29
26
|
import { loadAgentResources } from './agent-skills.mjs';
|
|
30
27
|
import { loadPiagentThemes } from '../runtime/terminal/terminal-style.mjs';
|
|
31
28
|
import { alwaysBypass, restorePermission, recordBypass, permissionSetter } from './permission.mjs';
|
|
@@ -463,19 +460,21 @@ export class ManagedSession {
|
|
|
463
460
|
content: 'Review is stale: code changed after review. Ask for another review only after fixing blocking findings; under a required review the harness reviews the final patch.', details: this.review }, { triggerTurn: false });
|
|
464
461
|
}
|
|
465
462
|
// `harness`: the completion gate asks, not the main agent (its brief, its turn).
|
|
466
|
-
async delegate({ role, task, title, since, ...more }, signal, { harness = false } = {}) {
|
|
463
|
+
async delegate({ role: name, task, title, since, ...more }, signal, { harness = false } = {}) {
|
|
464
|
+
const role = ROLE_NAMES[name]; // the helper's working name -> its Harness role
|
|
467
465
|
if (!this.grant || !HELPER_ROLES.includes(role) || typeof task !== 'string' || !task.trim() || task.length > 12000 || this.helpers.has(role)) throw Error('managed-helper-unavailable');
|
|
468
466
|
const enabled = helperRoles(this.manifest);
|
|
469
|
-
if (!enabled.includes(role)) throw Error(`managed-helper-not-configured: the company Harness has no ${
|
|
467
|
+
if (!enabled.includes(role)) throw Error(`managed-helper-not-configured: the company Harness has no ${name} subagent${enabled.length ? `; use ${enabled.map(callName).join(', ')}` : ''}.`);
|
|
468
|
+
if (role === 'general' && (more.copies || more.split)) throw Error('managed-general-single: the general helper runs one at a time, each in its own clone; call it again for the next piece of work after merging the first.');
|
|
469
|
+
if (role === 'general') this.orphans = sweepGeneral(this);
|
|
470
470
|
// Studio lets each helper role run HELPER_CALLS times per run (one user
|
|
471
471
|
// message), each helper the member asked for counted (copies.mjs). Say so at once.
|
|
472
472
|
const asked = fanOut(this.session?.sessionManager, role, more, harness), fan = asked?.dropped ? null : asked;
|
|
473
|
-
if ((this.helperCalls.get(role) ?? 0) + (fan?.n ?? 1) > HELPER_CALLS) throw Error(`managed-helper-limit: the ${
|
|
473
|
+
if ((this.helperCalls.get(role) ?? 0) + (fan?.n ?? 1) > HELPER_CALLS) throw Error(`managed-helper-limit: the ${name} subagent already ran ${HELPER_CALLS} times for this user message. Go on with what it returned, or use it again after the next user message.`);
|
|
474
474
|
beforeDelegate(this.run, role, harness, { since, review: this.review, changed: (this.ownPaths?.size ?? 0) > 0, asked: !!fan });
|
|
475
475
|
const job = this.runHelper(role, task, signal, harness, title, role === 'review' && typeof since === 'string' && since.trim() ? since.trim() : null, fan); this.helpers.set(role, job); this.publishHelpers();
|
|
476
|
-
// While a helper works (a
|
|
477
|
-
//
|
|
478
|
-
// turn after 15 minutes without an event, that the turn is working.
|
|
476
|
+
// While a helper works (a general helper running a long suite) the main agent is quiet: a beat each
|
|
477
|
+
// minute tells the dashboard's watchdog (it stops a turn after 15 silent minutes) that the turn is working.
|
|
479
478
|
const beat = setInterval(() => this.notify({ type: 'managed_helper_progress', role }), this.helperBeatMs ?? HELPER_BEAT_MS); beat.unref?.();
|
|
480
479
|
try { return withDroppedNote(await job, role, asked); } finally { clearInterval(beat); this.helpers.delete(role); this.publishHelpers(); }
|
|
481
480
|
}
|
|
@@ -495,6 +494,7 @@ export class ManagedSession {
|
|
|
495
494
|
if (this.disposed) return;
|
|
496
495
|
this.disposed = true;
|
|
497
496
|
await this.session?.abort(); await Promise.allSettled([...this.helpers.values()]);
|
|
497
|
+
for (const root of this.liveClones ?? []) fs.rmSync(root, { recursive: true, force: true });
|
|
498
498
|
this.session?.dispose(); await this.boundary?.dispose(); await this.broker.dispose();
|
|
499
499
|
}
|
|
500
500
|
}
|
|
@@ -4,9 +4,7 @@ import { BoundaryTerminals } from './terminal-boundary.mjs';
|
|
|
4
4
|
import { ToolFreshness } from './tool-freshness.mjs';
|
|
5
5
|
import { BoundaryQueue } from './boundary-queue.mjs';
|
|
6
6
|
import { workspaceWorkerPins, verifyWorkspaceWorker } from './workspace-worker-pins.mjs'; import { snapshotRuntime } from './runtime-snapshot.mjs';
|
|
7
|
-
import fs from 'node:fs';
|
|
8
|
-
import os from 'node:os';
|
|
9
|
-
import path from 'node:path';
|
|
7
|
+
import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path';
|
|
10
8
|
import { execFileSync, spawn } from 'node:child_process';
|
|
11
9
|
import net from 'node:net';
|
|
12
10
|
import { StringDecoder } from 'node:string_decoder';
|
|
@@ -208,7 +206,7 @@ export class ManagedToolBoundary {
|
|
|
208
206
|
// readOnly: the project cannot be written. commands: bash is offered all
|
|
209
207
|
// the same (a helper that runs checks); it writes only to its temporary
|
|
210
208
|
// directory, and has no network.
|
|
211
|
-
constructor({ cwd, sdkRoot, protectedRoots = [], readOnly = false, commands = false, node = process.execPath, identity, userHome: home, searchTools = true, skills: skillFolders = [], proxyPorts }) {
|
|
209
|
+
constructor({ cwd, sdkRoot, protectedRoots = [], readOnly = false, commands = false, node = process.execPath, identity, userHome: home, searchTools = true, skills: skillFolders = [], readRoots = [], proxyPorts }) {
|
|
212
210
|
// macOS: Seatbelt. Linux, and Windows through WSL2: bubblewrap (linux-sandbox.mjs).
|
|
213
211
|
this.linux = process.platform === 'linux';
|
|
214
212
|
if (process.platform !== 'darwin' && !(this.linux && BWRAP)) throw new Error('managed-sandbox-unavailable');
|
|
@@ -249,6 +247,8 @@ export class ManagedToolBoundary {
|
|
|
249
247
|
const skills = [...new Set(skillFolders.flatMap(dir => { try { return [fs.realpathSync(dir)]; } catch { return []; } }))]
|
|
250
248
|
.filter(dir => dir !== '/' && !/[\x00-\x1f\x7f]/.test(dir) && !narrowed.some(root => inside(root, dir)) && !inside(userHome, dir)
|
|
251
249
|
&& (!inside(dir, userHome) || agentFolders.some(root => inside(dir, root)) || !path.relative(userHome, dir).startsWith('.')));
|
|
250
|
+
// readRoots: folders the runtime opens for reading only (the general helper's borrowed git objects).
|
|
251
|
+
skills.push(...readRoots.map(dir => fs.realpathSync(dir)).filter(dir => dir !== '/' && !narrowed.some(root => inside(root, dir))));
|
|
252
252
|
this.userHome = userHome;
|
|
253
253
|
// Only for commands the user approved (run_with_network); credentials stay
|
|
254
254
|
// denied, and so do reference folders: what a command with network can
|
|
@@ -119,22 +119,6 @@ export function reviewFindings(text) {
|
|
|
119
119
|
}
|
|
120
120
|
return null;
|
|
121
121
|
}
|
|
122
|
-
// The verify helper re-checks a result on its own: claims against the code and
|
|
123
|
-
// primary sources, the repository's checks on the current code.
|
|
124
|
-
export const VERIFY_PROMPT = `You verify a result for the company coding assistant, independently of whoever produced it. For each claim or requirement in the brief: read the code it is about, run the repository checks that cover it with bash, and confirm external facts against primary sources (official documentation, the project's own repository) with web_search and web_fetch. Commands run offline and the project is read-only for them: a check that must write into the project or reach the network fails for that reason. Report such a claim as unverifiable and say why; do not work around the sandbox. Never mark a claim pass without evidence you saw. ${BRIEF_CHECK} End your answer with one JSON object in a \`\`\`json block: {"verdicts":[{"claim":"what was checked","status":"pass|fail|unverifiable","evidence":"file:line, the command and its result, or the source URL"}],${BRIEF_ISSUES_JSON},"summary":"one sentence"}. Use an empty brief_issues list when the brief is sound. You cannot change files or delegate.`;
|
|
125
|
-
// The verifier's verdicts from its answer; null when it gave no valid JSON.
|
|
126
|
-
export function verifyVerdicts(text) {
|
|
127
|
-
const blocks = [...String(text).matchAll(/```json\s*([\s\S]*?)```/g)].map(m => m[1]);
|
|
128
|
-
for (const raw of blocks.reverse()) {
|
|
129
|
-
try {
|
|
130
|
-
const value = JSON.parse(raw);
|
|
131
|
-
if (!Array.isArray(value?.verdicts)) continue;
|
|
132
|
-
return value.verdicts.filter(v => ['pass', 'fail', 'unverifiable'].includes(v?.status) && typeof v.claim === 'string' && v.claim.trim()).slice(0, 40)
|
|
133
|
-
.map(v => ({ status: v.status, claim: clip(v.claim, 500), evidence: clip(v.evidence, 500) }));
|
|
134
|
-
} catch { /* not this block */ }
|
|
135
|
-
}
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
122
|
const where = f => f.file ? ` ${f.file}${f.line ? ':' + f.line : ''}` : '';
|
|
139
123
|
export const findingText = f => `[${f.severity}]${where(f)} — ${f.issue}${f.evidence ? `\n Evidence: ${f.evidence}` : ''}`;
|
|
140
124
|
|
|
@@ -170,10 +154,10 @@ export class RunProcess {
|
|
|
170
154
|
reviews: 0, blocking: 0, blockingOpen: 0, fixLoops: 0, planUpdated: false, planRefusals: new Set(), planSkipped: false, unplannedChange: false,
|
|
171
155
|
unknownTools: 0, changed: false, verified: false, docsOnly: false, approvalDeclined: false, reviewed: false, reviewUnavailable: false, outcome: null, legacyOutcome: null,
|
|
172
156
|
// The objection ledger (ledger.mjs), disagreements handed to the
|
|
173
|
-
// member, the
|
|
157
|
+
// member, the reviews the main agent
|
|
174
158
|
// asked for, and where the harness last sent the agent back (to read
|
|
175
159
|
// its answer and see whether the code moved).
|
|
176
|
-
ledger: [], disputes: [],
|
|
160
|
+
ledger: [], disputes: [], mainReviews: 0, minorFindings: 0,
|
|
177
161
|
sentAt: null, sentDigest: null, rejudged: false, reviewSettled: false,
|
|
178
162
|
// How much of the suite the member wants run (check-questions.mjs).
|
|
179
163
|
scopeChoice: null, scopeWords: null, fullAsked: asksFullChecks(request), pushAllowedOn: undefined,
|
|
@@ -356,27 +340,6 @@ export async function completionGate(managed, run, signal) {
|
|
|
356
340
|
}
|
|
357
341
|
}
|
|
358
342
|
}
|
|
359
|
-
// Claims the verify subagent marked failed, under a required verify: back
|
|
360
|
-
// to the agent once, then checked again with its answer; still failing,
|
|
361
|
-
// the member decides.
|
|
362
|
-
if (policy.verify === 'require' && run.verifyFails.length && !run.claimsDone) {
|
|
363
|
-
const fails = run.verifyFails, list = fails.map((v, i) => `${i + 1}. ${v.claim}${v.evidence ? `\n Evidence: ${v.evidence}` : ''}`).join('\n'), claims = fails.slice(0, 10).map(v => ({ claim: v.claim }));
|
|
364
|
-
if (!run.claimsAsked) {
|
|
365
|
-
run.claimsAsked = true; run.claimsAt = session.messages.length;
|
|
366
|
-
await send(`Harness: the verify subagent marked ${fails.length} claim(s) as failed:\n${list}\nFix the cause, or reject a verdict with resolve_objection (what you checked and why). The verify subagent then checks these claims again with your answer.`, { phase: 'claims', claims });
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
if (!run.claimsRechecked) {
|
|
370
|
-
run.claimsRechecked = true;
|
|
371
|
-
await note("Harness: the verify subagent checks the failed claims again with the main agent's answer.", { phase: 'rejudge', role: 'verify' });
|
|
372
|
-
try { await managed.delegate({ role: 'verify', task: `Check these claims again on the current code; they were marked failed before:\n${list}\n\nThe main agent's answer:\n${answerSince(run.claimsAt) || '(no answer)'}${decisionsText(run, 'verify')}\n\nA claim passes only on evidence you see yourself. It stays failed when it does not hold and the result the member asked for depends on it. When the answer shows the claim is not part of what the member asked and the result does not depend on it, mark it unverifiable and say why.` }, signal, { harness: true }); }
|
|
373
|
-
catch { if (stopped()) { interrupted = run.changed; break; } }
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
run.claimsDone = true; run.disputes.push({ source: 'verify' });
|
|
377
|
-
for (const item of ledgerOf(run)) if (item.source === 'verify' && ['open', 'accepted', 'rejected'].includes(item.status)) setStatus(item, 'disputed');
|
|
378
|
-
await note(`Harness: the main agent and the verify subagent still disagree after two rounds; the member decides.\n${list}`, { phase: 'dispute', role: 'verify', claims });
|
|
379
|
-
}
|
|
380
343
|
// Plan "require": a checklist left with open steps is the next round's
|
|
381
344
|
// (continuation.mjs), after this run's checks and review.
|
|
382
345
|
// A rejected premise objection goes back to its helper; then what the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ManagedFailure = { role: "main" | "scout" | "research" | "
|
|
1
|
+
export type ManagedFailure = { role: "main" | "scout" | "research" | "advisor" | "general" | "review"; code: string; kind: string; requestId: string | null; local: boolean };
|
|
2
2
|
export function failureKind(code: string): string;
|
|
3
3
|
export function failureIsLocal(code: string): boolean;
|
|
4
4
|
export function failureCode(raw: unknown): string;
|
|
@@ -113,7 +113,7 @@ const CODES = {
|
|
|
113
113
|
// Studio is stopped or restarting behind it, or the way to it failed.
|
|
114
114
|
'studio-gateway-unavailable': ['unreachable', "Studio's gateway answered that Studio is not reachable"],
|
|
115
115
|
};
|
|
116
|
-
const ROLES = { main: 'main agent', scout: 'scout subagent', research: 'research subagent',
|
|
116
|
+
const ROLES = { main: 'main agent', scout: 'scout subagent', research: 'research subagent', advisor: 'advisor subagent', general: 'general subagent', review: 'review subagent' };
|
|
117
117
|
const REQUEST = /\b([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b/;
|
|
118
118
|
const PROVIDER_SERVER_ERROR = /^(?:Error Code )?(?:server_error|api_error|internal_error|internal_server_error|overloaded_error|undefined):|An error occurred while processing your request|"type"\s*:\s*"(?:api_error|overloaded_error)"|\bOverloaded\b/i;
|
|
119
119
|
const NETWORK = /fetch failed|econnrefused|econnreset|enotfound|ehostunreach|etimedout|socket hang up|network|connection error|terminated|timed? ?out|aborted due to timeout/i;
|
|
@@ -203,8 +203,8 @@ export function parseFailure(raw) {
|
|
|
203
203
|
const text = String(raw ?? '');
|
|
204
204
|
if (!text.trim()) return null;
|
|
205
205
|
const code = failureCode(text);
|
|
206
|
-
const role = /^Agent Watch (main agent|(?:scout|research|
|
|
207
|
-
?? /(scout|research|
|
|
206
|
+
const role = /^Agent Watch (main agent|(?:scout|research|advisor|general|review) subagent): /.exec(text)?.[1]?.split(' ')[0]
|
|
207
|
+
?? /(scout|research|advisor|general|review) subagent/.exec(text)?.[1] ?? 'main';
|
|
208
208
|
const request = failureIsLocal(code) ? null : REQUEST.exec(text)?.[1] ?? null;
|
|
209
209
|
return { role, code, kind: failureKind(code), requestId: request, local: failureIsLocal(code) };
|
|
210
210
|
}
|