@piagent/platform 1.8.0 → 1.9.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/AGENTS.md +7 -0
- package/CHANGELOG.md +132 -2
- package/README.md +45 -70
- package/README.vi.md +34 -61
- package/architecture/layers.json +16 -3
- package/catalog/capabilities.json +14 -7
- package/catalog/claude-model-additions.json +42 -0
- package/docs/auto-delegation-policy.md +2 -0
- package/docs/command-reference-vietnamese.md +68 -90
- package/docs/context-window-policy.md +5 -5
- package/docs/distribution-standard.md +13 -16
- package/docs/en/architecture.md +9 -1
- package/docs/herdr-workflow.md +2 -3
- package/docs/languages.json +4 -4
- package/docs/managed-local-acceptance.md +403 -0
- package/docs/mcp-and-tools.md +5 -7
- package/docs/memory-policy.md +3 -3
- package/docs/model-options.md +2 -2
- package/docs/oauth-providers.md +1 -1
- package/docs/operator-manual-vietnamese.md +82 -127
- package/docs/package-architecture-notes.md +4 -4
- package/docs/piagent-core-improvement-plan.md +3 -5
- package/docs/piagent-core-plan-checkpoint-2026-09-06.json +2 -13
- package/docs/piagent-runtime-benchmark-execution-plan.md +10 -8
- package/docs/project-adapters.md +3 -3
- package/docs/project-onboarding.md +15 -16
- package/docs/publishing-for-teams.md +3 -1
- package/docs/quality-benchmark.md +2 -0
- package/docs/quickstart-vietnamese.md +23 -24
- package/docs/release-install-policy.md +3 -2
- package/docs/research/harness-next-evidence-review.md +7 -7
- package/docs/runtime-harness-standard.md +2 -0
- package/docs/runtime-policy-design.md +2 -0
- package/docs/runtime-quality-baseline.md +2 -0
- package/docs/subagent-orchestration-capabilities.md +1 -1
- package/docs/subagents-and-multiagent.md +10 -8
- package/docs/task-implementation-contract.md +2 -0
- package/docs/task-lifecycle-vietnamese.md +2 -0
- package/docs/team-onboarding.md +28 -59
- package/docs/usage-observability.md +2 -2
- package/docs/vi/architecture.md +9 -1
- package/docs/workflow-recipes.md +6 -10
- package/docs-site/architecture.html +4 -3
- package/docs-site/assets/docs.css +80 -0
- package/docs-site/benchmark.html +7 -1
- package/docs-site/commands.html +37 -23
- package/docs-site/content/architecture.html +2 -2
- package/docs-site/content/benchmark.html +5 -0
- package/docs-site/content/commands.html +35 -22
- package/docs-site/content/ecosystem.html +144 -0
- package/docs-site/content/en/architecture.html +1 -1
- package/docs-site/content/en/benchmark.html +5 -0
- package/docs-site/content/en/commands.html +30 -13
- package/docs-site/content/en/ecosystem.html +144 -0
- package/docs-site/content/en/index.html +24 -9
- package/docs-site/content/en/quickstart.html +10 -12
- package/docs-site/content/en/security.html +1 -1
- package/docs-site/content/en/team.html +2 -2
- package/docs-site/content/en/whats-new.html +92 -1
- package/docs-site/content/en/workflows.html +29 -31
- package/docs-site/content/index.html +24 -9
- package/docs-site/content/quickstart.html +10 -12
- package/docs-site/content/security.html +2 -2
- package/docs-site/content/team.html +2 -2
- package/docs-site/content/whats-new.html +92 -1
- package/docs-site/content/workflows.html +36 -38
- package/docs-site/context-engine.html +2 -1
- package/docs-site/ecosystem.html +283 -0
- package/docs-site/en/architecture.html +3 -2
- package/docs-site/en/benchmark.html +7 -1
- package/docs-site/en/commands.html +32 -14
- package/docs-site/en/context-engine.html +2 -1
- package/docs-site/en/ecosystem.html +283 -0
- package/docs-site/en/index.html +30 -14
- package/docs-site/en/mcp-approval.html +2 -1
- package/docs-site/en/mcp-auth.html +2 -1
- package/docs-site/en/mcp-servers.html +2 -1
- package/docs-site/en/mcp.html +2 -1
- package/docs-site/en/permissions.html +2 -1
- package/docs-site/en/profiles.html +2 -1
- package/docs-site/en/quickstart.html +17 -18
- package/docs-site/en/runtime.html +2 -1
- package/docs-site/en/scope.html +2 -1
- package/docs-site/en/security.html +6 -5
- package/docs-site/en/team.html +4 -3
- package/docs-site/en/whats-new.html +100 -8
- package/docs-site/en/workflows.html +34 -35
- package/docs-site/index.html +30 -14
- package/docs-site/mcp-approval.html +2 -1
- package/docs-site/mcp-auth.html +2 -1
- package/docs-site/mcp-servers.html +2 -1
- package/docs-site/mcp.html +2 -1
- package/docs-site/permissions.html +2 -1
- package/docs-site/profiles.html +2 -1
- package/docs-site/quickstart.html +17 -18
- package/docs-site/runtime.html +2 -1
- package/docs-site/scope.html +2 -1
- package/docs-site/security.html +7 -6
- package/docs-site/team.html +4 -3
- package/docs-site/whats-new.html +100 -8
- package/docs-site/workflows.html +41 -42
- package/evals/fs5-causal-arm.v1.json +2 -2
- package/evals/private-holdout-v1/CURRENT_BOUNDARY.md +17 -0
- package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
- package/package.json +8 -7
- package/packages/piagent-core/README.md +15 -23
- package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +2 -0
- package/packages/piagent-core/extensions/experiment-loop-policy.ts +75 -0
- package/packages/piagent-core/extensions/local-state-path.js +15 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +108 -960
- package/packages/piagent-core/extensions/policy-core.js +2 -2
- package/packages/piagent-core/extensions/source-checkout-paths.ts +54 -0
- package/packages/piagent-core/managed/broker-client.mjs +49 -0
- package/packages/piagent-core/managed/capabilities.mjs +28 -0
- package/packages/piagent-core/managed/client-agent.mjs +6 -0
- package/packages/piagent-core/managed/compaction.mjs +63 -0
- package/packages/piagent-core/managed/doctor.mjs +107 -0
- package/packages/piagent-core/managed/helper-roles.mjs +57 -0
- package/packages/piagent-core/managed/native-catalog.mjs +42 -0
- package/packages/piagent-core/managed/patch-snapshot.mjs +48 -0
- package/packages/piagent-core/managed/repository-operation.mjs +84 -0
- package/packages/piagent-core/managed/request-stream.mjs +130 -0
- package/packages/piagent-core/managed/resource-loader.mjs +42 -0
- package/packages/piagent-core/managed/review-snapshot-worker.mjs +27 -0
- package/packages/piagent-core/managed/sandbox-diagnostic.mjs +12 -0
- package/packages/piagent-core/managed/search-fallback.mjs +93 -0
- package/packages/piagent-core/managed/session.mjs +493 -0
- package/packages/piagent-core/managed/store.d.mts +3 -0
- package/packages/piagent-core/managed/store.mjs +64 -0
- package/packages/piagent-core/managed/tool-boundary.mjs +275 -0
- package/packages/piagent-core/managed/tool-worker.mjs +36 -0
- package/packages/piagent-core/managed/toolchain.mjs +120 -0
- package/packages/piagent-core/managed/web-fetch.mjs +105 -0
- package/packages/piagent-core/managed/web-search.mjs +131 -0
- package/packages/piagent-core/managed/workflow.mjs +326 -0
- package/packages/piagent-core/package.json +2 -5
- package/packages/piagent-core/policies/base-policy.json +80 -26
- package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +3 -3
- package/packages/piagent-core/runtime/hooks/input-hook.ts +35 -196
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +2 -0
- package/packages/piagent-core/runtime/inspection/approval-broker.ts +9 -1
- package/packages/piagent-core/runtime/managed-failure.d.mts +10 -0
- package/packages/piagent-core/runtime/managed-failure.mjs +166 -0
- package/packages/piagent-core/runtime/orchestration/minimal-delegation-policy.ts +8 -29
- package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +1 -1
- package/packages/piagent-core/runtime/policy/block-remedy.ts +5 -3
- package/packages/piagent-core/runtime/registration/context-commands.ts +1 -1
- package/packages/piagent-core/runtime/registration/knowledge-tools.ts +7 -7
- package/packages/piagent-core/runtime/registration/policy-tools.ts +2 -2
- package/packages/piagent-core/runtime/registration/session-commands.ts +11 -26
- package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +1 -1
- package/packages/piagent-core/runtime/session/system-prompt.ts +6 -7
- package/packages/piagent-core/runtime/workflows/webui-runtime-command.ts +6 -6
- package/packages/piagent-core/runtime/workflows/webui-workflow.ts +6 -11
- package/packages/piagent-core/security/sensitive-text.js +9 -0
- package/packages/piagent-core/skills/piagent-ops/SKILL.md +8 -10
- package/packages/piagent-core/skills/test-audit/SKILL.md +101 -0
- package/packages/piagent-core/subagents/piagent-planner.md +1 -1
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-DMU7Xe_O.js → MarkdownMessage-BRPIMCRh.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/index-DNJ1vH5d.js +228 -0
- package/packages/piagent-webui/dist/client/assets/{index-DVahUVWy.css → index-DgSmcnP8.css} +1 -1
- package/packages/piagent-webui/dist/client/index.html +2 -2
- package/packages/piagent-webui/extension/piagent-webui.ts +11 -11
- package/packages/piagent-webui/extension/session-stream-adapter.ts +3 -1
- package/packages/piagent-webui/extension/terminal-session-adapter.ts +6 -1
- package/packages/piagent-webui/gateway/company-relay.ts +308 -0
- package/packages/piagent-webui/gateway/control-socket.ts +39 -12
- package/packages/piagent-webui/gateway/gateway-service.ts +109 -28
- package/packages/piagent-webui/gateway/gateway-session-stream.ts +58 -2
- package/packages/piagent-webui/gateway/managed-gateway.mjs +41 -0
- package/packages/piagent-webui/gateway/managed-launch.ts +145 -0
- package/packages/piagent-webui/gateway/managed-projection.ts +20 -0
- package/packages/piagent-webui/gateway/pi-host.ts +22 -4
- package/packages/piagent-webui/gateway/runtime-session-controls.ts +5 -2
- package/packages/piagent-webui/gateway/session-approval-authority.ts +28 -0
- package/packages/piagent-webui/gateway/session-catalog.ts +11 -3
- package/packages/piagent-webui/gateway/session-command-controller.ts +8 -2
- package/packages/piagent-webui/gateway/session-file-facts.ts +25 -0
- package/packages/piagent-webui/gateway/session-inspection-registry.ts +40 -9
- package/packages/piagent-webui/gateway/session-list-cache.ts +68 -0
- package/packages/piagent-webui/gateway/session-runtime-ownership.ts +8 -1
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +20 -27
- package/packages/piagent-webui/ownership/managed-terminal-lease.ts +27 -0
- package/packages/piagent-webui/ownership/profile-state.ts +2 -0
- package/packages/piagent-webui/ownership/session-lease-store.ts +22 -1
- package/packages/piagent-webui/server/approval-projection.ts +9 -0
- package/packages/piagent-webui/server/core-inspection-provider.ts +4 -9
- package/packages/piagent-webui/server/loopback-server.ts +37 -1
- package/packages/piagent-webui/server/message-stop-reason.ts +11 -0
- package/packages/piagent-webui/server/tool-call-summary.ts +185 -0
- package/packages/piagent-webui/server/transcript-projection.ts +86 -3
- package/packs/engineering-base/pack.json +4 -0
- package/schemas/piagent-webui/runtime-command-v1.schema.json +1 -2
- package/schemas/piagent-webui/session-catalog-v1.schema.json +8 -0
- package/schemas/piagent-webui/transcript-v1.schema.json +94 -1
- package/scripts/build-docs-site.mjs +23 -12
- package/scripts/check-public-wording.mjs +5 -6
- package/scripts/check-runtime-advisories.mjs +23 -6
- package/scripts/claude-model-additions.mjs +33 -0
- package/scripts/configure-model-scope.sh +30 -6
- package/scripts/configure-subagents.sh +1 -1
- package/scripts/explain-command.mjs +5 -6
- package/scripts/init-project.sh +0 -2
- package/scripts/install-global.sh +3 -2
- package/scripts/model-scope-prune.mjs +75 -0
- package/scripts/phase-tools-evaluation.mjs +9 -4
- package/scripts/piagent-cli.mjs +51 -2
- package/scripts/piagent-dashboard.mjs +27 -4
- package/scripts/piagent-installations.mjs +26 -0
- package/scripts/piagent-studio.mjs +155 -0
- package/scripts/runtime-advisory-exceptions.json +29 -0
- package/scripts/setup.sh +10 -6
- package/scripts/team-doctor.sh +20 -1
- package/scripts/typescript-loader.mjs +6 -0
- package/scripts/verify-local.sh +2 -2
- package/templates/global/settings.json +3 -1
- package/templates/project/.pi/project-context.md +3 -3
- package/templates/project/AGENTS.md +4 -5
- package/docs/piagent-core-reasoning-research-2026-09-06.md +0 -225
- package/docs/piagent-core-research-sources-2026-09-06.json +0 -416
- package/packages/piagent-webui/dist/client/assets/index-Cui9AAV_.js +0 -176
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Web search for company sessions: the provider's own search tool (Codex
|
|
2
|
+
// `web_search`, Claude `web_search_20250305`) through Studio with the current
|
|
3
|
+
// run token, so queries stay with the company accounts and are metered like
|
|
4
|
+
// any other request. The request carries the grant's model and effort, which
|
|
5
|
+
// Studio requires to match. A role on an API-key vendor model searches through
|
|
6
|
+
// the company search pool instead (searchThroughPool).
|
|
7
|
+
import { CLIENT_AGENT } from './client-agent.mjs';
|
|
8
|
+
|
|
9
|
+
const INSTRUCTIONS = 'Search the web for the request. Answer concisely with the facts found and cite every source URL. Prefer official documentation and primary sources.';
|
|
10
|
+
|
|
11
|
+
function sourceList(sources) {
|
|
12
|
+
const seen = new Set(), out = [];
|
|
13
|
+
for (const source of sources) {
|
|
14
|
+
if (typeof source?.url !== 'string' || !/^https?:\/\//.test(source.url)) continue;
|
|
15
|
+
const url = source.url.replace(/[?&]utm_source=openai$/, '');
|
|
16
|
+
if (seen.has(url)) continue;
|
|
17
|
+
seen.add(url); out.push({ title: typeof source.title === 'string' && source.title.trim() ? source.title.trim() : url, url });
|
|
18
|
+
}
|
|
19
|
+
return out.slice(0, 20);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function* events(text) {
|
|
23
|
+
for (const line of text.split('\n')) {
|
|
24
|
+
if (!line.startsWith('data:')) continue;
|
|
25
|
+
const data = line.slice(5).trim();
|
|
26
|
+
if (!data || data === '[DONE]') continue;
|
|
27
|
+
try { yield JSON.parse(data); } catch { /* skip keep-alives */ }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseCodex(text) {
|
|
32
|
+
let output = [];
|
|
33
|
+
const items = [];
|
|
34
|
+
for (const event of events(text)) {
|
|
35
|
+
if (event.type === 'response.output_item.done' && event.item) items.push(event.item);
|
|
36
|
+
if ((event.type === 'response.completed' || event.type === 'response.done') && Array.isArray(event.response?.output)) output = event.response.output;
|
|
37
|
+
}
|
|
38
|
+
if (!output.length) output = items;
|
|
39
|
+
// Sources the answer cites come first, then the other pages searched.
|
|
40
|
+
const answer = [], cited = [], searched = [];
|
|
41
|
+
for (const item of output) {
|
|
42
|
+
if (item?.type === 'message') for (const part of item.content ?? []) {
|
|
43
|
+
if (typeof part?.text === 'string') answer.push(part.text);
|
|
44
|
+
for (const note of part?.annotations ?? []) if (note?.type === 'url_citation') cited.push(note);
|
|
45
|
+
}
|
|
46
|
+
if (item?.type === 'web_search_call') for (const group of [item.action?.sources, item.sources, item.results]) {
|
|
47
|
+
for (const source of Array.isArray(group) ? group : []) searched.push({ url: source?.url, title: source?.title });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { answer: answer.join('\n').trim(), sources: sourceList([...cited, ...searched]) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseClaude(text) {
|
|
54
|
+
const blocks = [], sources = [];
|
|
55
|
+
let current = null;
|
|
56
|
+
for (const event of events(text)) {
|
|
57
|
+
if (event.type === 'content_block_start') {
|
|
58
|
+
current = event.content_block ?? null;
|
|
59
|
+
if (current?.type === 'text') blocks.push(current.text ?? '');
|
|
60
|
+
if (current?.type === 'web_search_tool_result' && Array.isArray(current.content)) for (const result of current.content) sources.push(result);
|
|
61
|
+
}
|
|
62
|
+
if (event.type === 'content_block_delta') {
|
|
63
|
+
if (event.delta?.type === 'text_delta' && current?.type === 'text') blocks[blocks.length - 1] += event.delta.text ?? '';
|
|
64
|
+
if (event.delta?.type === 'citations_delta') sources.push(event.delta.citation);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { answer: blocks.join('').trim(), sources: sourceList(sources) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function searchThroughStudio({ provider, origin, token, roleId, model, effort, query, domains, signal, fetchImpl = fetch }) {
|
|
71
|
+
if (typeof query !== 'string' || !query.trim() || query.length > 2000) throw new Error('web-search-query-invalid');
|
|
72
|
+
const allowed = Array.isArray(domains) ? domains.filter((domain) => typeof domain === 'string' && /^[a-z0-9.-]{1,253}$/i.test(domain)).slice(0, 20) : [];
|
|
73
|
+
const claude = provider === 'claude';
|
|
74
|
+
const body = claude
|
|
75
|
+
? { model, max_tokens: 4096, system: INSTRUCTIONS, stream: true, messages: [{ role: 'user', content: query }],
|
|
76
|
+
tools: [{ type: 'web_search_20250305', name: 'web_search', max_uses: 5, ...(allowed.length ? { allowed_domains: allowed } : {}) }],
|
|
77
|
+
...(effort ? { thinking: { type: 'adaptive' }, output_config: { effort } } : {}) }
|
|
78
|
+
: { model, instructions: INSTRUCTIONS, stream: true, store: false, input: [{ role: 'user', content: [{ type: 'input_text', text: query }] }],
|
|
79
|
+
tools: [{ type: 'web_search', ...(allowed.length ? { filters: { allowed_domains: allowed } } : {}) }],
|
|
80
|
+
include: ['web_search_call.action.sources'], tool_choice: 'required', ...(effort ? { reasoning: { effort } } : {}) };
|
|
81
|
+
const response = await fetchImpl(`${origin}${claude ? '/claude/v1/messages' : '/v1/responses'}`, {
|
|
82
|
+
method: 'POST', signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(120_000)]) : AbortSignal.timeout(120_000),
|
|
83
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', Accept: 'text/event-stream', 'User-Agent': CLIENT_AGENT, 'X-Session-Id': roleId,
|
|
84
|
+
...(claude ? { 'anthropic-version': '2023-06-01' } : {}) },
|
|
85
|
+
body: JSON.stringify(body) });
|
|
86
|
+
const text = await response.text();
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
let code = `http-${response.status}`;
|
|
89
|
+
try { code = JSON.parse(text)?.error?.code ?? code; } catch { /* not JSON */ }
|
|
90
|
+
throw new Error(`web-search-failed: ${String(code).slice(0, 80)}`);
|
|
91
|
+
}
|
|
92
|
+
const result = claude ? parseClaude(text) : parseCodex(text);
|
|
93
|
+
if (!result.answer && !result.sources.length) throw new Error('web-search-empty');
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Web search from the company search pool (Studio's /v1/search): the team's
|
|
98
|
+
// pool keys first, then keyless providers. It takes no model or effort, so a
|
|
99
|
+
// role on any model can search; Studio meters it per member.
|
|
100
|
+
const POOL_ANSWER_CHARS = 4000, POOL_SNIPPET_CHARS = 500;
|
|
101
|
+
export async function searchThroughPool({ origin, token, roleId, query, domains, signal, fetchImpl = fetch }) {
|
|
102
|
+
if (typeof query !== 'string' || !query.trim() || query.length > 2000) throw new Error('web-search-query-invalid');
|
|
103
|
+
const allowed = Array.isArray(domains) ? domains.filter((domain) => typeof domain === 'string' && /^[a-z0-9.-]{1,253}$/i.test(domain)).map((domain) => domain.toLowerCase()).slice(0, 20) : [];
|
|
104
|
+
const response = await fetchImpl(`${origin}/v1/search`, {
|
|
105
|
+
method: 'POST', signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(150_000)]) : AbortSignal.timeout(150_000),
|
|
106
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', 'User-Agent': CLIENT_AGENT, 'X-Session-Id': roleId },
|
|
107
|
+
body: JSON.stringify({ query: query.trim(), max_results: 5, ...(allowed.length ? { include_domains: allowed } : {}) }) });
|
|
108
|
+
const text = await response.text();
|
|
109
|
+
// A Studio without the search pool: say what the member can do instead.
|
|
110
|
+
if (response.status === 404) throw new Error('managed-web-search-unavailable');
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
let code = `http-${response.status}`;
|
|
113
|
+
try { code = JSON.parse(text)?.error?.code ?? code; } catch { /* not JSON */ }
|
|
114
|
+
throw new Error(`web-search-failed: ${String(code).slice(0, 80)}`);
|
|
115
|
+
}
|
|
116
|
+
let body;
|
|
117
|
+
try { body = JSON.parse(text); } catch { throw new Error('web-search-failed: invalid_response'); }
|
|
118
|
+
const results = Array.isArray(body?.results) ? body.results : [];
|
|
119
|
+
const sources = sourceList(results).map((source) => {
|
|
120
|
+
const hit = results.find((result) => result?.url === source.url);
|
|
121
|
+
const snippet = typeof hit?.content === 'string' ? hit.content.trim().slice(0, POOL_SNIPPET_CHARS) : '';
|
|
122
|
+
return snippet ? { ...source, snippet } : source;
|
|
123
|
+
});
|
|
124
|
+
const answer = typeof body?.answer === 'string' ? body.answer.trim().slice(0, POOL_ANSWER_CHARS) : '';
|
|
125
|
+
if (!answer && !sources.length) throw new Error('web-search-empty');
|
|
126
|
+
return { answer, sources, provider: typeof body?.provider === 'string' ? body.provider : 'pool' };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function searchResultText({ answer, sources }) {
|
|
130
|
+
return [answer || 'No summary returned.', sources.length ? 'Sources:\n' + sources.map((source, index) => `${index + 1}. ${source.title} — ${source.url}${source.snippet ? '\n ' + source.snippet.replace(/\s+/g, ' ') : ''}`).join('\n') : ''].filter(Boolean).join('\n\n');
|
|
131
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { reviewUnavailableReason } from './patch-snapshot.mjs';
|
|
4
|
+
import { failureCode } from '../runtime/managed-failure.mjs';
|
|
5
|
+
|
|
6
|
+
// The process layer of the company harness: a plan for multi-step work, the
|
|
7
|
+
// repository's checks after a code change, an independent review of the patch.
|
|
8
|
+
// Studio's Harness sets the policy. "suggest" tells the agent and records what
|
|
9
|
+
// happened; "require" is enforced here, in code, before a code-changing turn
|
|
10
|
+
// ends: the agent is sent back at most max_fix_loops times.
|
|
11
|
+
const MODES = ['off', 'suggest', 'require'];
|
|
12
|
+
export function workflowPolicy(manifest) {
|
|
13
|
+
const config = manifest?.harness?.configuration ?? {}, w = config.workflow ?? {};
|
|
14
|
+
const mode = value => MODES.includes(value) ? value : 'suggest';
|
|
15
|
+
const loops = Number.isInteger(w.max_fix_loops) && w.max_fix_loops >= 0 && w.max_fix_loops <= 3 ? w.max_fix_loops : 2;
|
|
16
|
+
// A review needs a review helper; without one in the Harness it is off.
|
|
17
|
+
return { plan: mode(w.plan), verify: mode(w.verify), review: config.review ? mode(w.review) : 'off', maxFixLoops: loops };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Checks come from the repository: a "## Checks" section in AGENTS.md (code
|
|
21
|
+
// block lines or `command` bullets), else what its manifests imply.
|
|
22
|
+
const HEADING = /^#{1,6}\s+(checks?|verification|kiểm tra)\s*$/i;
|
|
23
|
+
export function declaredChecks(agentsFiles = []) {
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const file of agentsFiles) {
|
|
26
|
+
let inside = false, fence = false;
|
|
27
|
+
for (const line of String(file.content ?? '').split('\n')) {
|
|
28
|
+
if (!fence && /^#{1,6}\s/.test(line)) { inside = HEADING.test(line.trim()); continue; }
|
|
29
|
+
if (!inside) continue;
|
|
30
|
+
if (/^\s*(```|~~~)/.test(line)) { fence = !fence; continue; }
|
|
31
|
+
const command = (fence ? line : line.match(/^\s*[-*]\s+`([^`]+)`/)?.[1] ?? '').trim();
|
|
32
|
+
if (command && !command.startsWith('#') && command.length <= 200 && !out.includes(command) && out.length < 10) out.push(command);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
export function detectedChecks(dir) {
|
|
38
|
+
const has = name => { try { const s = fs.lstatSync(path.join(dir, name)); return s.isFile() && s.size <= 1024 * 1024; } catch { return false; } };
|
|
39
|
+
const out = [];
|
|
40
|
+
if (has('package.json')) {
|
|
41
|
+
try {
|
|
42
|
+
const scripts = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).scripts ?? {};
|
|
43
|
+
const runner = has('pnpm-lock.yaml') ? 'pnpm' : has('yarn.lock') ? 'yarn' : has('bun.lock') || has('bun.lockb') ? 'bun' : 'npm';
|
|
44
|
+
for (const name of ['typecheck', 'lint', 'test']) if (typeof scripts[name] === 'string') out.push(name === 'test' && runner === 'npm' ? 'npm test' : `${runner} run ${name}`);
|
|
45
|
+
} catch { /* not JSON: nothing implied */ }
|
|
46
|
+
}
|
|
47
|
+
if (has('go.mod')) out.push('go vet ./...', 'go test ./...');
|
|
48
|
+
if (has('Package.swift')) out.push('swift build', 'swift test');
|
|
49
|
+
if (has('Cargo.toml')) out.push('cargo check', 'cargo test');
|
|
50
|
+
if (has('pyproject.toml') || has('pytest.ini')) out.push('pytest');
|
|
51
|
+
return out.slice(0, 6);
|
|
52
|
+
}
|
|
53
|
+
export function repositoryChecks(agentsFiles, cwd, top = cwd) {
|
|
54
|
+
const declared = declaredChecks(agentsFiles);
|
|
55
|
+
if (declared.length) return { source: 'agents', commands: declared };
|
|
56
|
+
const detected = detectedChecks(cwd);
|
|
57
|
+
return { source: 'detected', commands: detected.length || cwd === top ? detected : detectedChecks(top) };
|
|
58
|
+
}
|
|
59
|
+
// A command that verifies code: a declared check, or a known test, type,
|
|
60
|
+
// lint or build runner. Text tools that merely mention one do not count.
|
|
61
|
+
const RUNNER = /\b(npm|pnpm|yarn|bun)\s+(run\s+)?(test|lint|typecheck|type-check|check|build|verify)\b|\bnode\s+--test\b|\b(tsc|vitest|jest|eslint|biome|ruff|mypy|pyright|pytest|rspec|phpunit)\b|\bplaywright\s+test\b|\bpython3?\s+-m\s+(pytest|unittest|mypy)\b|\bgo\s+(test|vet|build)\b|\bcargo\s+(test|check|clippy|build)\b|\bswift\s+(test|build)\b|\bxcodebuild\b.*\b(test|build)\b|\bmake\s+(test|check|lint)\b|\b(gradle|gradlew|mvn)\b.*\b(test|check|verify)\b|\bdotnet\s+(test|build)\b|\bdeno\s+(test|check|lint)\b/;
|
|
62
|
+
// A command that only reads: each part starts with a reading tool and nothing
|
|
63
|
+
// is written to a file. Anything else may change files.
|
|
64
|
+
const READER = /^(ls|cat|head|tail|wc|grep|egrep|rg|find|fd|sed|awk|cut|sort|uniq|tr|echo|printf|pwd|which|stat|file|du|df|tree|jq|diff|cmp|basename|dirname|realpath|date|true|cd|git\s+(status|diff|log|show|branch|ls-files|rev-parse|blame))\b/;
|
|
65
|
+
export function isReadOnlyCommand(command) {
|
|
66
|
+
const text = String(command ?? '').trim();
|
|
67
|
+
if (!text || />(?!>|\s*&\d|\s*\/dev\/null)|\btee\b|\bsed\s+(-[a-zA-Z]*i|--in-place)|\s-(delete|exec|execdir|fprint|ok)\b|\bxargs\b|\$\(|`/.test(text)) return false;
|
|
68
|
+
return text.split(/&&|\|\||;|\||\n/).every(part => !part.trim() || READER.test(part.trim()));
|
|
69
|
+
}
|
|
70
|
+
export function isCheckCommand(command, declared = []) {
|
|
71
|
+
const text = String(command ?? '').replace(/\s+/g, ' ').trim();
|
|
72
|
+
if (!text || /^(echo|printf|grep|rg|cat|sed|awk|head|tail|less|git|ls|find)\b/.test(text)) return false;
|
|
73
|
+
return declared.some(d => text.includes(d)) || RUNNER.test(text);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// update_plan: the agent's checklist, kept as a session entry so it survives
|
|
77
|
+
// compaction and "continue", and shown to the member beside the conversation.
|
|
78
|
+
export const PLAN_ENTRY = 'agent-watch-plan';
|
|
79
|
+
const STATUS = ['pending', 'in_progress', 'completed'];
|
|
80
|
+
export function normalizePlan(args) {
|
|
81
|
+
const plan = Array.isArray(args?.plan) ? args.plan : [];
|
|
82
|
+
if (plan.length < 1 || plan.length > 30 || plan.some(p => typeof p?.step !== 'string' || !p.step.trim() || p.step.length > 200 || !STATUS.includes(p.status)))
|
|
83
|
+
throw Error('managed-plan-invalid: send 1 to 30 steps, each {step, status} with status pending, in_progress or completed.');
|
|
84
|
+
if (plan.filter(p => p.status === 'in_progress').length > 1) throw Error('managed-plan-invalid: mark at most one step in_progress.');
|
|
85
|
+
return { plan: plan.map(p => ({ step: p.step.trim(), status: p.status })), explanation: typeof args.explanation === 'string' ? args.explanation.slice(0, 300) : '' };
|
|
86
|
+
}
|
|
87
|
+
export function currentPlan(manager) {
|
|
88
|
+
return manager.getEntries().filter(e => e.type === 'custom' && e.customType === PLAN_ENTRY).at(-1)?.data ?? null;
|
|
89
|
+
}
|
|
90
|
+
export const unfinished = plan => !!plan?.plan?.some(p => p.status !== 'completed');
|
|
91
|
+
export function planText(plan) {
|
|
92
|
+
return plan?.plan?.map((p, i) => `${i + 1}. [${p.status === 'completed' ? 'x' : p.status === 'in_progress' ? '>' : ' '}] ${p.step}`).join('\n') ?? '';
|
|
93
|
+
}
|
|
94
|
+
export function planTool(run) {
|
|
95
|
+
return { name: 'update_plan', label: 'Plan',
|
|
96
|
+
description: 'Keep a short checklist for work with several steps. Send the whole list each time; mark the step you work on in_progress and finished steps completed. The member sees the checklist next to the conversation.',
|
|
97
|
+
parameters: { type: 'object', properties: { explanation: { type: 'string', maxLength: 300 }, plan: { type: 'array', minItems: 1, maxItems: 30,
|
|
98
|
+
items: { type: 'object', properties: { step: { type: 'string', minLength: 1, maxLength: 200 }, status: { type: 'string', enum: STATUS } }, required: ['step', 'status'], additionalProperties: false } } },
|
|
99
|
+
required: ['plan'], additionalProperties: false },
|
|
100
|
+
// Some models send the list as a JSON string; it is read as the list.
|
|
101
|
+
prepareArguments: args => {
|
|
102
|
+
if (typeof args?.plan !== 'string') return args;
|
|
103
|
+
try { const plan = JSON.parse(args.plan); return Array.isArray(plan) ? { ...args, plan } : args; } catch { return args; }
|
|
104
|
+
},
|
|
105
|
+
execute: async (_id, args) => run(normalizePlan(args)) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// The guidance a policy adds to the main agent's system prompt.
|
|
109
|
+
export function workflowPrompt(policy, checks) {
|
|
110
|
+
const lines = [];
|
|
111
|
+
if (policy.plan !== 'off') lines.push(`- Plan: for work with several steps keep a checklist with update_plan and update it as you go.${policy.plan === 'require' ? ' For a complex task, file edits are refused until a plan exists, and a checklist left unfinished comes back to you before the turn ends.' : ''}`);
|
|
112
|
+
if (policy.verify !== 'off') {
|
|
113
|
+
const list = checks.commands.length ? `${checks.commands.map(c => '`' + c + '`').join(', ')}${checks.source === 'detected' ? ' (detected; the repository declares none in AGENTS.md "## Checks")' : ''}` : 'none declared: choose the tests, type check or build that cover the change';
|
|
114
|
+
lines.push(`- Verify: after changing code, run the repository checks that cover the change and fix failures before you finish. Repository checks: ${list}.${policy.verify === 'require' ? ' The turn does not end until a check has passed on the final code.' : ''}`);
|
|
115
|
+
}
|
|
116
|
+
if (policy.review !== 'off') lines.push(policy.review === 'require' ? '- Review: the harness reviews the final patch with an independent reviewer; blocking findings come back to you to fix.'
|
|
117
|
+
: '- Review: before you finish a code change, ask the review helper (delegate role "review") to review the patch, then fix blocking findings.');
|
|
118
|
+
return lines.length ? `\n\nProcess for code changes (company Harness):\n${lines.join('\n')}\nEnd-to-end tests that start a local server and a browser run through run_with_network, which the user approves. If a step cannot be done here (a check needs a database or another service), say so plainly; do not try to install or start services to get around the sandbox.\nA command stops after 10 minutes and anything left running in the background stops when the turn ends: run the checks that cover your change first and a long suite in parts, not in the background.` : '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const REVIEW_PROMPT = 'You review a patch for the company coding assistant. The patch snapshot below is immutable; read files to check the code around it. Report only problems that matter: incorrect behaviour, regressions, security issues, parts of the request that are missing, changed behaviour without tests. Do not report style or preferences. Classify each finding: blocking (must be fixed before the work is done), major, or minor. End your answer with one JSON object in a ```json block: {"findings":[{"severity":"blocking|major|minor","file":"path","line":1,"issue":"what is wrong","evidence":"why"}],"summary":"one sentence"}. Use an empty findings list when the patch is fine. You cannot change files or delegate.';
|
|
122
|
+
const clip = (value, n) => typeof value === 'string' ? value.slice(0, n) : '';
|
|
123
|
+
// The reviewer's findings from its answer; null when it gave no valid JSON.
|
|
124
|
+
export function reviewFindings(text) {
|
|
125
|
+
const blocks = [...String(text).matchAll(/```json\s*([\s\S]*?)```/g)].map(m => m[1]);
|
|
126
|
+
for (const raw of blocks.reverse()) {
|
|
127
|
+
try {
|
|
128
|
+
const value = JSON.parse(raw);
|
|
129
|
+
if (!Array.isArray(value?.findings)) continue;
|
|
130
|
+
return value.findings.filter(f => ['blocking', 'major', 'minor'].includes(f?.severity) && typeof f.issue === 'string' && f.issue.trim()).slice(0, 20)
|
|
131
|
+
.map(f => ({ severity: f.severity, file: clip(f.file, 300), line: Number.isSafeInteger(f.line) && f.line > 0 ? f.line : null, issue: clip(f.issue, 500), evidence: clip(f.evidence, 500) }));
|
|
132
|
+
} catch { /* not this block */ }
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
// The verify helper re-checks a result on its own: claims against the code and
|
|
137
|
+
// primary sources, the repository's checks on the current code.
|
|
138
|
+
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. 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"}],"summary":"one sentence"}. You cannot change files or delegate.';
|
|
139
|
+
// The verifier's verdicts from its answer; null when it gave no valid JSON.
|
|
140
|
+
export function verifyVerdicts(text) {
|
|
141
|
+
const blocks = [...String(text).matchAll(/```json\s*([\s\S]*?)```/g)].map(m => m[1]);
|
|
142
|
+
for (const raw of blocks.reverse()) {
|
|
143
|
+
try {
|
|
144
|
+
const value = JSON.parse(raw);
|
|
145
|
+
if (!Array.isArray(value?.verdicts)) continue;
|
|
146
|
+
return value.verdicts.filter(v => ['pass', 'fail', 'unverifiable'].includes(v?.status) && typeof v.claim === 'string' && v.claim.trim()).slice(0, 40)
|
|
147
|
+
.map(v => ({ status: v.status, claim: clip(v.claim, 500), evidence: clip(v.evidence, 500) }));
|
|
148
|
+
} catch { /* not this block */ }
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const where = f => f.file ? ` ${f.file}${f.line ? ':' + f.line : ''}` : '';
|
|
153
|
+
export const findingText = f => `[${f.severity}]${where(f)} — ${f.issue}${f.evidence ? `\n Evidence: ${f.evidence}` : ''}`;
|
|
154
|
+
|
|
155
|
+
// A change made by a turn that stopped before its process ran (stopped by the
|
|
156
|
+
// member, a failed request, a crash) stays the next turn's to settle: that run
|
|
157
|
+
// compares against the code before the change ("continue"). A turn whose
|
|
158
|
+
// process ran to its end and reported what is missing is settled as reported;
|
|
159
|
+
// later messages are not sent back for it again.
|
|
160
|
+
export const BASELINE_ENTRY = 'agent-watch-baseline';
|
|
161
|
+
export function pendingBaseline(manager) {
|
|
162
|
+
const last = manager.getEntries().filter(e => e.type === 'custom' && e.customType === BASELINE_ENTRY).at(-1)?.data;
|
|
163
|
+
return last && last.settled === false && typeof last.digest === 'string' ? last.digest : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// A model that cannot write a valid plan is not stuck forever: after this many
|
|
167
|
+
// refused answers its edits go through and the run reports that.
|
|
168
|
+
export const PLAN_REFUSALS = 3;
|
|
169
|
+
|
|
170
|
+
// What happened in one run (one member message), for the gate and the report.
|
|
171
|
+
export class RunProcess {
|
|
172
|
+
constructor(policy, { request, complex, checks }) {
|
|
173
|
+
Object.assign(this, { policy, request: String(request ?? ''), complex, checks, startDigest: null, mutations: 0, lastCheck: null, checksRun: 0, checksFailed: 0,
|
|
174
|
+
reviews: 0, blocking: 0, blockingOpen: 0, fixLoops: 0, planUpdated: false, planRefusals: new Set(), planSkipped: false, planAsked: false, unplannedChange: false,
|
|
175
|
+
unknownTools: 0, changed: false, verified: false, reviewed: false, reviewUnavailable: false, outcome: null });
|
|
176
|
+
}
|
|
177
|
+
// Plan "require" on a complex task that has no plan yet.
|
|
178
|
+
planMissing(plan) {
|
|
179
|
+
return this.policy.plan === 'require' && this.complex && !this.planUpdated && !unfinished(plan);
|
|
180
|
+
}
|
|
181
|
+
// Plan "require": the edits of a complex task are refused until a plan
|
|
182
|
+
// exists: write and edit, and any command once a command has changed files
|
|
183
|
+
// without one (models also edit through the shell). `answer` names the
|
|
184
|
+
// model answer the call came from, so calls sent together count once.
|
|
185
|
+
beforeEdit(name, plan, answer) {
|
|
186
|
+
if (!this.planMissing(plan) || !(['write', 'edit'].includes(name) || (name === 'bash' && this.unplannedChange))) return;
|
|
187
|
+
if (!this.planRefusals.has(answer) && this.planRefusals.size >= PLAN_REFUSALS) { this.planSkipped = true; return; }
|
|
188
|
+
this.planRefusals.add(answer);
|
|
189
|
+
throw Error('managed-plan-required: this is a multi-step task. Write a checklist with update_plan first, then repeat this call.');
|
|
190
|
+
}
|
|
191
|
+
// A command changed files while the required plan was missing.
|
|
192
|
+
changedWithoutPlan() {
|
|
193
|
+
this.unplannedChange = true; this.planSkipped = true;
|
|
194
|
+
}
|
|
195
|
+
async afterTool(name, args, ok, digestOf) {
|
|
196
|
+
if (name === 'bash' && isCheckCommand(args?.command, this.checks.commands)) {
|
|
197
|
+
this.checksRun += 1; if (!ok) this.checksFailed += 1;
|
|
198
|
+
this.lastCheck = { ok, digest: await digestOf(), mutations: this.mutations };
|
|
199
|
+
// A command may change files, so it counts. Without a git repository to
|
|
200
|
+
// compare the code against, one that only reads does not: counting every
|
|
201
|
+
// command reported code changes in turns that only read and checked.
|
|
202
|
+
} else if (['write', 'edit'].includes(name) && ok || name === 'bash' && (this.startDigest !== null || !isReadOnlyCommand(args?.command))) this.mutations += 1;
|
|
203
|
+
}
|
|
204
|
+
verifiedOn(digest) {
|
|
205
|
+
const c = this.lastCheck;
|
|
206
|
+
return !!c?.ok && (digest !== null && c.digest !== null ? c.digest === digest : c.mutations === this.mutations);
|
|
207
|
+
}
|
|
208
|
+
// Version 2 (a broker advertising "process-v2") adds whether code was
|
|
209
|
+
// changed without the required plan and how many calls named no tool.
|
|
210
|
+
report(plan, version = 1) {
|
|
211
|
+
const steps = this.planUpdated || unfinished(plan) ? plan?.plan ?? [] : [];
|
|
212
|
+
return { version, policy: { plan: this.policy.plan, verify: this.policy.verify, review: this.policy.review }, changed: this.changed,
|
|
213
|
+
plan_steps: steps.length, plan_done: steps.filter(p => p.status === 'completed').length, checks: Math.min(this.checksRun, 1000), checks_failed: Math.min(this.checksFailed, 1000),
|
|
214
|
+
verified: this.verified, reviews: Math.min(this.reviews, 1000), reviewed: this.reviewed, blocking: Math.min(this.blocking, 1000), blocking_open: Math.min(this.blockingOpen, this.blocking, 1000),
|
|
215
|
+
fix_loops: this.fixLoops, outcome: this.outcome ?? (this.changed ? 'interrupted' : 'no_change'),
|
|
216
|
+
...(version === 2 ? { plan_skipped: this.planSkipped, unknown_tools: Math.min(this.unknownTools, 1000) } : {}) };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Before a turn ends: decide whether code changed, whether a check passed on
|
|
221
|
+
// the final code and whether the final patch was reviewed. With "require" the
|
|
222
|
+
// harness acts (asks for checks, runs the review, sends blocking findings back)
|
|
223
|
+
// within max_fix_loops; every turn that changed code ends with a status note.
|
|
224
|
+
export async function completionGate(managed, run, signal) {
|
|
225
|
+
const session = managed.session, policy = run.policy;
|
|
226
|
+
const digestOf = async () => { try { return (await managed.patchSnapshot()).digest; } catch { return null; } };
|
|
227
|
+
const stopped = () => signal?.aborted || ['aborted', 'error'].includes(session.messages.findLast(m => m.role === 'assistant')?.stopReason);
|
|
228
|
+
const send = (content, details) => session.sendCustomMessage({ customType: 'agent-watch-process', content, display: true, details }, { triggerTurn: true });
|
|
229
|
+
let digest = null, interrupted = false, verifyGaveUp = false;
|
|
230
|
+
for (let step = 0; step < 12; step += 1) {
|
|
231
|
+
digest = await digestOf();
|
|
232
|
+
run.changed = run.startDigest !== null && digest !== null ? digest !== run.startDigest : run.mutations > 0;
|
|
233
|
+
if (!run.changed) break;
|
|
234
|
+
if (stopped()) { interrupted = true; break; }
|
|
235
|
+
run.verified = run.verifiedOn(digest);
|
|
236
|
+
const review = managed.review, fresh = !!review && !review.stale && digest !== null && review.patchDigest === digest;
|
|
237
|
+
run.reviewed = fresh;
|
|
238
|
+
run.blockingOpen = fresh ? review.blocking ?? 0 : run.blockingOpen;
|
|
239
|
+
// Out of fix rounds the check stays unmet, but a required review still runs.
|
|
240
|
+
if (policy.verify === 'require' && !run.verified && !verifyGaveUp && run.fixLoops >= policy.maxFixLoops) verifyGaveUp = true;
|
|
241
|
+
if (policy.verify === 'require' && !run.verified && !verifyGaveUp) {
|
|
242
|
+
run.fixLoops += 1;
|
|
243
|
+
const list = run.checks.commands.length ? run.checks.commands.map(c => '`' + c + '`').join(', ') : 'the tests, type check or build that cover the change';
|
|
244
|
+
await send(`Harness process check: code changed in this turn, but no check has passed on the current code. Run the checks that cover your change (${list}) and fix what fails. If a check cannot run here (it needs a database, a service or the network) or fails for a reason unrelated to your change, say so plainly instead of trying to install or start services.`,
|
|
245
|
+
{ phase: 'verify', loop: run.fixLoops, maxLoops: policy.maxFixLoops, lastCheckFailed: run.lastCheck?.ok === false });
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (policy.review === 'require' && !fresh) {
|
|
249
|
+
const plan = currentPlan(session.sessionManager);
|
|
250
|
+
const brief = `Review the current patch against the member's request.\n\nMember's request:\n${run.request.slice(0, 6000)}\n\nPlan:\n${planText(plan) || '(none)'}\n\nChecks: ${run.verified ? 'a repository check passed on this code' : 'no check has passed on this code'}.`;
|
|
251
|
+
try { await managed.delegate({ role: 'review', task: brief }, signal); }
|
|
252
|
+
catch (error) {
|
|
253
|
+
run.reviewUnavailable = !stopped(); run.reviewUnavailableReason = reviewUnavailableReason(error?.message); interrupted = stopped();
|
|
254
|
+
// Why the review could not run, as a code only (the report says "failed").
|
|
255
|
+
if (run.reviewUnavailable) try { session.sessionManager.appendCustomEntry('agent-watch-failure-detail', { role: 'review', code: failureCode(error?.message) }); } catch { /* the report still says it */ }
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (policy.review === 'require' && fresh && (review.blocking ?? 0) > 0) {
|
|
261
|
+
if (run.fixLoops >= policy.maxFixLoops) break;
|
|
262
|
+
run.fixLoops += 1;
|
|
263
|
+
const blocking = (review.findings ?? []).filter(f => f.severity === 'blocking');
|
|
264
|
+
await send(`Harness review of the current patch found ${blocking.length} blocking issue(s):\n${blocking.map((f, i) => `${i + 1}. ${findingText(f)}`).join('\n')}\nFix them, or explain why an issue is not real. The patch is reviewed again after your change.`,
|
|
265
|
+
{ phase: 'review', loop: run.fixLoops, maxLoops: policy.maxFixLoops, findings: blocking.slice(0, 10) });
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
// Plan "require": a checklist written in this turn is brought up to date
|
|
269
|
+
// once before the turn ends (models often write it and never touch it again).
|
|
270
|
+
const plan = currentPlan(session.sessionManager);
|
|
271
|
+
if (policy.plan === 'require' && run.planUpdated && !run.planAsked && unfinished(plan)) {
|
|
272
|
+
run.planAsked = true;
|
|
273
|
+
const open = plan.plan.filter(p => p.status !== 'completed').length;
|
|
274
|
+
await send(`Harness process check: your checklist still has ${open} step(s) not marked completed:\n${planText(plan)}\nUpdate it with update_plan so it shows what was done. For a step you did not do, do it or say plainly why it is left.`,
|
|
275
|
+
{ phase: 'plan', planOpen: open });
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
// However the code was changed, a complex task under plan "require" that
|
|
281
|
+
// never had a plan is reported as changed without it.
|
|
282
|
+
if (run.changed && run.planMissing(currentPlan(session.sessionManager))) run.planSkipped = true;
|
|
283
|
+
run.outcome = !run.changed ? 'no_change' : interrupted ? 'interrupted' : run.blockingOpen > 0 ? 'blocking_open'
|
|
284
|
+
: policy.verify !== 'off' && !run.verified ? 'unverified' : run.reviewUnavailable ? 'review_unavailable' : policy.review !== 'off' && !run.reviewed ? 'unreviewed' : 'clean';
|
|
285
|
+
const settled = run.outcome !== 'interrupted';
|
|
286
|
+
session.sessionManager.appendCustomEntry(BASELINE_ENTRY, { digest: settled ? digest : run.startDigest, settled, outcome: run.outcome });
|
|
287
|
+
if (!run.changed) return;
|
|
288
|
+
const finalPlan = currentPlan(session.sessionManager), planOpen = run.planUpdated ? finalPlan?.plan?.filter(p => p.status !== 'completed').length ?? 0 : 0;
|
|
289
|
+
const status = { verified: run.verified, reviewed: run.reviewed, blockingOpen: run.blockingOpen, checks: run.checksRun, fixLoops: run.fixLoops,
|
|
290
|
+
...(planOpen ? { planOpen } : {}), ...(run.planSkipped ? { planSkipped: true } : {}) };
|
|
291
|
+
const words = [run.verified ? 'a check passed on the final code' : policy.verify === 'off' ? null : 'no check passed on the final code',
|
|
292
|
+
run.reviewed ? (run.blockingOpen ? `review: ${run.blockingOpen} blocking issue(s) still open` : 'review: no blocking issue') : policy.review === 'off' ? null : run.reviewUnavailable ? `review could not run (${run.reviewUnavailableReason})` : 'not reviewed',
|
|
293
|
+
run.planSkipped ? 'code was changed before the required plan existed' : null, planOpen ? `plan: ${planOpen} step(s) not completed` : null].filter(Boolean);
|
|
294
|
+
await session.sendCustomMessage({ customType: 'agent-watch-process', content: `Process status for this turn: code changed; ${words.join('; ') || 'no process step configured'}.`, display: true,
|
|
295
|
+
details: { phase: 'final', outcome: run.outcome, ...status, ...(run.reviewUnavailable ? { reviewUnavailable: run.reviewUnavailableReason } : {}), policy: { verify: policy.verify, review: policy.review } } }, { triggerTurn: false });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// The run's process sees each edit and command: plan guard before, check
|
|
299
|
+
// results and changes after. `session` is the ManagedSession (its run, latest
|
|
300
|
+
// main answer count, code digest and review refresh).
|
|
301
|
+
export function processEditTools(session, tools) {
|
|
302
|
+
return tools.map(tool => {
|
|
303
|
+
if (!['write', 'edit', 'bash'].includes(tool.name)) return tool;
|
|
304
|
+
const execute = tool.execute;
|
|
305
|
+
return { ...tool, execute: async (...args) => {
|
|
306
|
+
const plan = currentPlan(session.session.sessionManager);
|
|
307
|
+
session.run?.beforeEdit(tool.name, plan, session.mainAnswers);
|
|
308
|
+
// While a required plan is missing, a command that changes files
|
|
309
|
+
// counts as an edit: the code is compared after each command.
|
|
310
|
+
const watch = tool.name === 'bash' && !!session.run?.planMissing(plan) && !session.run.unplannedChange && session.run.startDigest !== null;
|
|
311
|
+
let ok = false;
|
|
312
|
+
try {
|
|
313
|
+
const result = await execute(...args); ok = true;
|
|
314
|
+
if (watch && session.run) {
|
|
315
|
+
const digest = await session.digest();
|
|
316
|
+
if (digest !== null && digest !== session.run.startDigest) {
|
|
317
|
+
session.run.changedWithoutPlan();
|
|
318
|
+
return { ...result, content: [...(result.content ?? []), { type: 'text', text: '\n[Harness] This command changed files before the required plan existed. Write the checklist with update_plan now; further edits and commands are refused until it exists.' }] };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
finally { await session.run?.afterTool(tool.name, args[1], ok, () => session.digest()); await session.refreshReview(); }
|
|
324
|
+
} };
|
|
325
|
+
});
|
|
326
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piagent/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": true,
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"description": "Pi agent package: guard extension,
|
|
7
|
+
"description": "Pi agent package: guard extension, skills, and policy defaults.",
|
|
8
8
|
"homepage": "https://piagent.io.vn",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -23,9 +23,6 @@
|
|
|
23
23
|
"skills": [
|
|
24
24
|
"skills"
|
|
25
25
|
],
|
|
26
|
-
"prompts": [
|
|
27
|
-
"prompts"
|
|
28
|
-
],
|
|
29
26
|
"subagents": {
|
|
30
27
|
"agents": [
|
|
31
28
|
"subagents"
|
|
@@ -99,19 +99,49 @@
|
|
|
99
99
|
"execPolicy": {
|
|
100
100
|
"defaultMode": "enforce",
|
|
101
101
|
"bannedPrefixSuggestions": [
|
|
102
|
-
[
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
[
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
[
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
[
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
102
|
+
[
|
|
103
|
+
"python"
|
|
104
|
+
],
|
|
105
|
+
[
|
|
106
|
+
"python3"
|
|
107
|
+
],
|
|
108
|
+
[
|
|
109
|
+
"node"
|
|
110
|
+
],
|
|
111
|
+
[
|
|
112
|
+
"node",
|
|
113
|
+
"-e"
|
|
114
|
+
],
|
|
115
|
+
[
|
|
116
|
+
"bash"
|
|
117
|
+
],
|
|
118
|
+
[
|
|
119
|
+
"bash",
|
|
120
|
+
"-lc"
|
|
121
|
+
],
|
|
122
|
+
[
|
|
123
|
+
"sh"
|
|
124
|
+
],
|
|
125
|
+
[
|
|
126
|
+
"sh",
|
|
127
|
+
"-c"
|
|
128
|
+
],
|
|
129
|
+
[
|
|
130
|
+
"zsh"
|
|
131
|
+
],
|
|
132
|
+
[
|
|
133
|
+
"zsh",
|
|
134
|
+
"-lc"
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
"git"
|
|
138
|
+
],
|
|
139
|
+
[
|
|
140
|
+
"sudo"
|
|
141
|
+
],
|
|
142
|
+
[
|
|
143
|
+
"env"
|
|
144
|
+
]
|
|
115
145
|
],
|
|
116
146
|
"rules": [
|
|
117
147
|
{
|
|
@@ -187,18 +217,42 @@
|
|
|
187
217
|
"piagent_trace_record"
|
|
188
218
|
],
|
|
189
219
|
"toolCapabilities": {
|
|
190
|
-
"bash": [
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
"
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
"
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
"
|
|
200
|
-
|
|
201
|
-
|
|
220
|
+
"bash": [
|
|
221
|
+
"shell"
|
|
222
|
+
],
|
|
223
|
+
"shell": [
|
|
224
|
+
"shell"
|
|
225
|
+
],
|
|
226
|
+
"exec": [
|
|
227
|
+
"shell"
|
|
228
|
+
],
|
|
229
|
+
"read": [
|
|
230
|
+
"filesystem-readonly"
|
|
231
|
+
],
|
|
232
|
+
"grep": [
|
|
233
|
+
"filesystem-readonly"
|
|
234
|
+
],
|
|
235
|
+
"find": [
|
|
236
|
+
"filesystem-readonly"
|
|
237
|
+
],
|
|
238
|
+
"ls": [
|
|
239
|
+
"filesystem-readonly"
|
|
240
|
+
],
|
|
241
|
+
"write": [
|
|
242
|
+
"filesystem-write"
|
|
243
|
+
],
|
|
244
|
+
"edit": [
|
|
245
|
+
"filesystem-write"
|
|
246
|
+
],
|
|
247
|
+
"apply_patch": [
|
|
248
|
+
"filesystem-write"
|
|
249
|
+
],
|
|
250
|
+
"browser": [
|
|
251
|
+
"browser"
|
|
252
|
+
],
|
|
253
|
+
"github": [
|
|
254
|
+
"github"
|
|
255
|
+
]
|
|
202
256
|
}
|
|
203
257
|
},
|
|
204
258
|
"externalActionPolicy": {
|
|
@@ -273,7 +327,7 @@
|
|
|
273
327
|
},
|
|
274
328
|
"orchestrationPolicy": {
|
|
275
329
|
"defaultMode": "solo-first",
|
|
276
|
-
"maxConcurrentSubagents":
|
|
330
|
+
"maxConcurrentSubagents": 2,
|
|
277
331
|
"defaultReviewLenses": [
|
|
278
332
|
"correctness",
|
|
279
333
|
"tests",
|
|
@@ -52,7 +52,7 @@ import type { AgentStartHookDependencies } from "./agent-start-types.ts";
|
|
|
52
52
|
export function registerAgentStartHook(pi: ExtensionAPI, dependencies: AgentStartHookDependencies): void {
|
|
53
53
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
54
54
|
const projectInstructions = rewriteLegacyProjectInstructions(event.systemPrompt);
|
|
55
|
-
const query =
|
|
55
|
+
const query = event.prompt;
|
|
56
56
|
const signal = classifyContextTask(query);
|
|
57
57
|
const uncertainSendContinuation = isUncertainSendContinuation(query);
|
|
58
58
|
// A mutation clarification still goes to the provider unchanged and never
|
|
@@ -68,7 +68,7 @@ export function registerAgentStartHook(pi: ExtensionAPI, dependencies: AgentStar
|
|
|
68
68
|
&& signal.paths.every((candidate) => matchesProtectedPath(candidate, readProtectedPaths));
|
|
69
69
|
const sessionTask = dependencies.activeTask(ctx);
|
|
70
70
|
const activeTask = sessionTask?.trace.outcome === "pending" ? sessionTask : undefined;
|
|
71
|
-
const runtimeIntakeMode =
|
|
71
|
+
const runtimeIntakeMode = undefined;
|
|
72
72
|
const runtimeIntake = Boolean(runtimeIntakeMode);
|
|
73
73
|
const compactMode = protectedOnlyTarget
|
|
74
74
|
? "protected"
|
|
@@ -154,7 +154,7 @@ export function registerAgentStartHook(pi: ExtensionAPI, dependencies: AgentStar
|
|
|
154
154
|
: undefined;
|
|
155
155
|
const durableResume = activeTask && resumed ? buildTaskResumeContext(activeTask, resumed) : undefined;
|
|
156
156
|
const selectedContext = contextMessage ?? durableResume;
|
|
157
|
-
const intake =
|
|
157
|
+
const intake: Awaited<ReturnType<AgentStartHookDependencies["startAutomaticTask"]>> = undefined;
|
|
158
158
|
if (intake?.task) observeTrajectorySync(ctx, dependencies.syncTrajectory?.(ctx, intake.task, { sourceHook: "agent-start", recommendationRef }), dependencies.telemetry);
|
|
159
159
|
if (!selectedContext && !intake) return systemPromptUpdate;
|
|
160
160
|
const discoveryItems = selectedContext?.customType === "piagent-context-pack-v2" && Array.isArray(selectedContext.details.selectedItems)
|