@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
|
@@ -1199,7 +1199,7 @@ export function evaluateExecPolicyCore(command, options) {
|
|
|
1199
1199
|
for (const pattern of policy.blockedCommandPatterns ?? []) {
|
|
1200
1200
|
if (!legacyPatternMatchesSegment(pattern, words)) continue;
|
|
1201
1201
|
matches.push(`forbid:legacy:${pattern}`);
|
|
1202
|
-
reasons.push(`Blocked
|
|
1202
|
+
reasons.push(`Blocked: the command runs ${pattern}, which the project policy forbids`);
|
|
1203
1203
|
}
|
|
1204
1204
|
|
|
1205
1205
|
const nested = extractNestedCommands(braceText, words);
|
|
@@ -1242,7 +1242,7 @@ export function evaluateExecPolicyCore(command, options) {
|
|
|
1242
1242
|
};
|
|
1243
1243
|
for (const pattern of policy.requireConfirmationPatterns ?? []) {
|
|
1244
1244
|
if (segments.some((entry) => confirmationPatternMatches(pattern, entry.words))) {
|
|
1245
|
-
reasons.push(`Confirmation required
|
|
1245
|
+
reasons.push(`Confirmation required: the command runs ${pattern}`);
|
|
1246
1246
|
break;
|
|
1247
1247
|
}
|
|
1248
1248
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { extractShellPathCandidates, normalizePathCandidate } from "./policy-core.js";
|
|
5
|
+
import { normalizeRelative } from "./shell-reach.ts";
|
|
6
|
+
|
|
7
|
+
// Where a path lands relative to the external source checkouts granted to this
|
|
8
|
+
// session (read-only roots under the shared cache).
|
|
9
|
+
|
|
10
|
+
function inside(root: string, target: string): boolean {
|
|
11
|
+
const relative = path.relative(root, target);
|
|
12
|
+
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function grantedSourceCheckoutRootForPath(cwd: string, candidate: string, roots: string[]): string | undefined {
|
|
16
|
+
if (roots.length === 0) return undefined;
|
|
17
|
+
const absolute = path.resolve(cwd, normalizePathCandidate(candidate));
|
|
18
|
+
let canonical: string;
|
|
19
|
+
try {
|
|
20
|
+
canonical = fs.realpathSync.native(absolute);
|
|
21
|
+
} catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return roots.find((root) => inside(root, canonical));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A shell command can name a file it is about to create. realpath fails for that
|
|
28
|
+
// file, so resolve its nearest existing parent instead; otherwise a new file in
|
|
29
|
+
// the cache, spelled through an alias such as /var for /private/var, escapes
|
|
30
|
+
// both the text match and the realpath match.
|
|
31
|
+
function shellTargetInsideGrantedSourceCheckout(cwd: string, candidate: string, roots: string[]): boolean {
|
|
32
|
+
if (grantedSourceCheckoutRootForPath(cwd, candidate, roots)) return true;
|
|
33
|
+
let parent = path.resolve(cwd, normalizePathCandidate(candidate));
|
|
34
|
+
const rest: string[] = [];
|
|
35
|
+
while (path.dirname(parent) !== parent) {
|
|
36
|
+
rest.unshift(path.basename(parent));
|
|
37
|
+
parent = path.dirname(parent);
|
|
38
|
+
let canonical: string;
|
|
39
|
+
try { canonical = fs.realpathSync.native(parent); } catch { continue; }
|
|
40
|
+
const target = path.join(canonical, ...rest);
|
|
41
|
+
return roots.some((root) => inside(root, target));
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function shellTouchesGrantedSourceCheckout(cwd: string, command: string, roots: string[]): boolean {
|
|
47
|
+
if (roots.length === 0) return false;
|
|
48
|
+
for (const root of roots) {
|
|
49
|
+
const relative = path.relative(cwd, root).split(path.sep).join("/");
|
|
50
|
+
if (command.includes(root) || (relative && command.includes(relative))) return true;
|
|
51
|
+
}
|
|
52
|
+
return extractShellPathCandidates(command)
|
|
53
|
+
.some((candidate) => shellTargetInsideGrantedSourceCheckout(cwd, normalizeRelative(cwd, candidate) ?? candidate, roots));
|
|
54
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
export class ManagedBrokerClient {
|
|
8
|
+
#child; #pending = new Map(); #tail = Promise.resolve(); #closed = false;
|
|
9
|
+
constructor({ executable, profileID }) {
|
|
10
|
+
if (!path.isAbsolute(executable) || fs.realpathSync(executable) !== executable || !/^[a-f0-9]{64}$/.test(profileID)) throw Error('managed-broker-binding-invalid');
|
|
11
|
+
this.#child = spawn(executable, ['managed-broker', '--profile', profileID], {
|
|
12
|
+
stdio: ['pipe', 'pipe', 'pipe'], env: { PATH: '/usr/bin:/bin', LANG: 'en_US.UTF-8' },
|
|
13
|
+
});
|
|
14
|
+
let pending = ''; const decoder = new StringDecoder('utf8');
|
|
15
|
+
const fail = () => { this.#closed = true; for (const item of this.#pending.values()) { clearTimeout(item.timer); item.reject(Error('managed-broker-disconnected')); } this.#pending.clear(); };
|
|
16
|
+
this.#child.stdin.on('error', fail); this.#child.on('error', fail); this.#child.on('close', fail);
|
|
17
|
+
this.#child.stderr.on('data', () => {});
|
|
18
|
+
this.#child.stdout.on('data', chunk => {
|
|
19
|
+
pending += decoder.write(chunk);
|
|
20
|
+
if (Buffer.byteLength(pending) > 2 * 1024 * 1024) { this.#child.kill(); fail(); return; }
|
|
21
|
+
for (;;) {
|
|
22
|
+
const end = pending.indexOf('\n'); if (end < 0) break;
|
|
23
|
+
const line = pending.slice(0, end); pending = pending.slice(end + 1);
|
|
24
|
+
try {
|
|
25
|
+
const result = JSON.parse(line), item = this.#pending.get(result.id);
|
|
26
|
+
if (!item) throw Error('unexpected-broker-response');
|
|
27
|
+
clearTimeout(item.timer); this.#pending.delete(result.id);
|
|
28
|
+
if (result.error) item.reject(Error(`managed-broker:${String(result.error).replace(/[^a-zA-Z_]/g, '').slice(0, 80)}`));
|
|
29
|
+
else item.resolve(result.result);
|
|
30
|
+
} catch { this.#child.kill(); fail(); }
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
request(action, args = {}) {
|
|
35
|
+
const next = this.#tail.then(() => new Promise((resolve, reject) => {
|
|
36
|
+
if (this.#closed) { reject(Error('managed-broker-disconnected')); return; }
|
|
37
|
+
const id = randomUUID();
|
|
38
|
+
const timer = setTimeout(() => { this.#child.kill(); reject(Error('managed-broker-timeout')); }, 25000);
|
|
39
|
+
this.#pending.set(id, { resolve, reject, timer });
|
|
40
|
+
this.#child.stdin.write(JSON.stringify({ ...args, id, action }) + '\n');
|
|
41
|
+
}));
|
|
42
|
+
this.#tail = next.catch(() => {}); return next;
|
|
43
|
+
}
|
|
44
|
+
async dispose() {
|
|
45
|
+
if (this.#closed) return;
|
|
46
|
+
try { await this.request('close'); } catch {}
|
|
47
|
+
this.#child.stdin.end(); this.#child.kill(); this.#closed = true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { nativeManagedModel, piProvider } from './native-catalog.mjs';
|
|
2
|
+
export const THINKING = ['off','minimal','low','medium','high','xhigh','max'];
|
|
3
|
+
export function nativeThinkingLevels(model) {
|
|
4
|
+
if (!model?.reasoning) return ['off'];
|
|
5
|
+
const mapping=model.thinkingLevelMap??{};
|
|
6
|
+
return THINKING.filter(level=>mapping[level]!==null && (!['xhigh','max'].includes(level)||mapping[level]!==undefined));
|
|
7
|
+
}
|
|
8
|
+
// A selected level must work for the current main route and at least one
|
|
9
|
+
// permitted candidate for every enabled helper. Studio rechecks availability
|
|
10
|
+
// and policy on run creation; this is capability UI, not an admission promise.
|
|
11
|
+
export function managedThinkingLevels(manifest, runtime, main) {
|
|
12
|
+
// A role whose level the Harness fixes does not narrow the member's choice.
|
|
13
|
+
const roles=Object.entries(manifest.harness.configuration).filter(([name,r])=>r&&Array.isArray(r.model_ids)&&(name==='main'||r.effort==null)).map(([,r])=>r);
|
|
14
|
+
return nativeThinkingLevels(main).filter(level=>(!Array.isArray(manifest.thinking_levels)||manifest.thinking_levels.includes(level)) && roles.every(role=>role.model_ids.some(id=>{
|
|
15
|
+
const item=manifest.models.find(m=>m.id===id);
|
|
16
|
+
if(!item)return false;
|
|
17
|
+
const native=nativeManagedModel(runtime,piProvider(item.owned_by),item.provider_model_id);
|
|
18
|
+
return native && nativeThinkingLevels(native).includes(level);
|
|
19
|
+
})));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Pi's rule for a level a model lacks: the next deeper one it has, else the
|
|
23
|
+
// next shallower one. Without a list every level stands.
|
|
24
|
+
export function nearestLevel(level, allowed) {
|
|
25
|
+
if (!Array.isArray(allowed) || allowed.length === 0 || allowed.includes(level)) return level;
|
|
26
|
+
const order = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'], at = Math.max(0, order.indexOf(level));
|
|
27
|
+
return order.slice(at).find(l => allowed.includes(l)) ?? order.slice(0, at).reverse().find(l => allowed.includes(l)) ?? allowed[0];
|
|
28
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
// How Piagent names itself to Studio on every company request, so Studio's
|
|
4
|
+
// reports show the tool and its version ("piagent/1.9.1").
|
|
5
|
+
const version = JSON.parse(fs.readFileSync(new URL('../../../package.json', import.meta.url), 'utf8')).version;
|
|
6
|
+
export const CLIENT_AGENT = `piagent/${version}`;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { failureCode } from '../runtime/managed-failure.mjs';
|
|
4
|
+
import { currentPlan, unfinished, planText } from './workflow.mjs';
|
|
5
|
+
|
|
6
|
+
// One message longer than a summary part (a pasted log) keeps its start and
|
|
7
|
+
// end for the summary; the transcript itself is unchanged.
|
|
8
|
+
function clip(message, tokens) {
|
|
9
|
+
const keep = tokens * 2, cut = text => text.length <= 2 * keep ? text : `${text.slice(0, keep)}\n[… ${text.length - 2 * keep} characters left out of this summary …]\n${text.slice(-keep)}`;
|
|
10
|
+
if (typeof message.content === 'string') return { ...message, content: cut(message.content) };
|
|
11
|
+
if (!Array.isArray(message.content)) return message;
|
|
12
|
+
return { ...message, content: message.content.map(part => part.type === 'text' ? { ...part, text: cut(part.text) } : part.type === 'thinking' ? { ...part, thinking: cut(part.thinking ?? '') } : part) };
|
|
13
|
+
}
|
|
14
|
+
function addUsage(a, b) {
|
|
15
|
+
const sum = (x, y) => (x ?? 0) + (y ?? 0);
|
|
16
|
+
return { ...a, input: sum(a.input, b.input), output: sum(a.output, b.output), cacheRead: sum(a.cacheRead, b.cacheRead), cacheWrite: sum(a.cacheWrite, b.cacheWrite),
|
|
17
|
+
totalTokens: sum(a.totalTokens, b.totalTokens), cost: Object.fromEntries(Object.keys(a.cost ?? {}).map(k => [k, sum(a.cost[k], b.cost?.[k])])) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Pi summarises everything older than the kept tail in one request. A
|
|
21
|
+
// conversation can be longer than the window of the model a run uses now
|
|
22
|
+
// (the harness moved from a 1M-token model to a 272k one, or Auto fell
|
|
23
|
+
// back), and that request is then refused, so the conversation could never
|
|
24
|
+
// shrink again. Such a history is summarised in parts that fit, each part
|
|
25
|
+
// updating the summary of the parts before.
|
|
26
|
+
export async function stageCompaction(session, api, sdk) {
|
|
27
|
+
const whole = session._runDefaultCompaction?.bind(session);
|
|
28
|
+
if (!whole || !session._summarizationRetryCallbacks) throw Error('managed-sdk-version-unqualified');
|
|
29
|
+
const { computeFileLists, formatFileOperations } = await import(pathToFileURL(path.join(sdk, 'dist/core/compaction/utils.js')));
|
|
30
|
+
// An unfinished plan is kept word for word: the summary would lose its steps.
|
|
31
|
+
const withPlan = result => {
|
|
32
|
+
const plan = currentPlan(session.sessionManager);
|
|
33
|
+
return unfinished(plan) ? { ...result, summary: `${result.summary}\n\nCurrent plan (kept verbatim; keep it up to date with update_plan):\n${planText(plan)}` } : result;
|
|
34
|
+
};
|
|
35
|
+
session._runDefaultCompaction = async (preparation, model, apiKey, headers, instructions, signal, env, reason) => {
|
|
36
|
+
const { settings, previousSummary } = preparation, room = model.contextWindow - settings.reserveTokens;
|
|
37
|
+
const history = [...preparation.messagesToSummarize, ...preparation.turnPrefixMessages];
|
|
38
|
+
const size = history.reduce((n, m) => n + api.estimateTokens(m), 0) + Math.ceil((previousSummary?.length ?? 0) / 4);
|
|
39
|
+
if (size + 4096 <= room) {
|
|
40
|
+
try { return withPlan(await whole(preparation, model, apiKey, headers, instructions, signal, env, reason)); }
|
|
41
|
+
// Token estimates are rough, and Studio takes at most 4 MiB per body:
|
|
42
|
+
// a single request refused as too large is retried in parts.
|
|
43
|
+
catch (error) { if (signal?.aborted || !['upstream_request_rejected', 'request_too_large'].includes(failureCode(error?.message))) throw error; }
|
|
44
|
+
}
|
|
45
|
+
// Half the window per part leaves room for dense text (Vietnamese, code),
|
|
46
|
+
// the previous summary and the answer.
|
|
47
|
+
const budget = Math.max(8192, Math.floor(room / 2) - settings.reserveTokens), parts = [];
|
|
48
|
+
for (const message of history) {
|
|
49
|
+
const fitted = clip(message, budget), tokens = api.estimateTokens(fitted), part = parts.at(-1);
|
|
50
|
+
if (part && part.tokens + tokens <= budget) { part.messages.push(fitted); part.tokens += tokens; }
|
|
51
|
+
else parts.push({ messages: [fitted], tokens });
|
|
52
|
+
}
|
|
53
|
+
let summary = previousSummary, usage;
|
|
54
|
+
for (const part of parts) {
|
|
55
|
+
const result = await api.generateSummaryWithUsage(part.messages, model, settings.reserveTokens, apiKey, headers, signal, instructions, summary,
|
|
56
|
+
session.thinkingLevel, session.agent.streamFunction, env, session.settingsManager.getRetrySettings(), session._summarizationRetryCallbacks({ source: 'compaction', reason }), undefined);
|
|
57
|
+
summary = result.text; usage = usage ? addUsage(usage, result.usage) : result.usage;
|
|
58
|
+
}
|
|
59
|
+
const { readFiles, modifiedFiles } = computeFileLists(preparation.fileOps);
|
|
60
|
+
return withPlan({ summary: summary + formatFileOperations(readFiles, modifiedFiles), firstKeptEntryId: preparation.firstKeptEntryId,
|
|
61
|
+
tokensBefore: preparation.tokensBefore, usage, details: { readFiles, modifiedFiles } });
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { ManagedToolBoundary } from './tool-boundary.mjs';
|
|
6
|
+
import { ensureSearchTools } from './toolchain.mjs';
|
|
7
|
+
import { nativeManagedModel, piProvider } from './native-catalog.mjs';
|
|
8
|
+
import { fetchPublicPage } from './web-fetch.mjs';
|
|
9
|
+
|
|
10
|
+
// `piagent studio --doctor`: what a company session needs on THIS machine,
|
|
11
|
+
// checked with the real sandbox and the real Agent Watch broker, without
|
|
12
|
+
// sending any model request. Run after installing on a new machine, or when
|
|
13
|
+
// tools misbehave. Required checks decide the exit code; optional ones only
|
|
14
|
+
// report what daily coding may miss (a compiler, Python, git identity).
|
|
15
|
+
const text = result => (result?.content ?? []).filter(part => part.type === 'text').map(part => part.text).join('\n').trim();
|
|
16
|
+
const first = value => String(value ?? '').split('\n')[0].slice(0, 160);
|
|
17
|
+
|
|
18
|
+
export async function runDoctor({ sdkRoot, origin, broker, network = true }) {
|
|
19
|
+
const results = [];
|
|
20
|
+
const check = async (label, required, run) => {
|
|
21
|
+
try { results.push({ label, required, ok: true, detail: first(await run()) }); }
|
|
22
|
+
catch (error) { results.push({ label, required, ok: false, detail: first(error?.message ?? error) }); }
|
|
23
|
+
};
|
|
24
|
+
const sdk = fs.realpathSync(sdkRoot);
|
|
25
|
+
await check('Pi host 0.87.1', true, () => {
|
|
26
|
+
const version = JSON.parse(fs.readFileSync(path.join(sdk, 'package.json'), 'utf8')).version;
|
|
27
|
+
if (version !== '0.87.1') throw Error(`found ${version}; company sessions need 0.87.1`);
|
|
28
|
+
return sdk;
|
|
29
|
+
});
|
|
30
|
+
await check('search tools (ripgrep, fd)', true, async () => { await ensureSearchTools(sdk); return 'ready'; });
|
|
31
|
+
const project = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'piagent-doctor-')));
|
|
32
|
+
let boundary;
|
|
33
|
+
try {
|
|
34
|
+
boundary = new ManagedToolBoundary({ cwd: project, sdkRoot: sdk, protectedRoots: [] });
|
|
35
|
+
const bash = async (command, options) => text(await boundary.invoke('bash', { command, timeout: 60 }, undefined, undefined, undefined, options));
|
|
36
|
+
await check('sandbox starts and runs a command', true, async () => { if (await bash('echo doctor-ok') !== 'doctor-ok') throw Error('unexpected output'); return 'bash'; });
|
|
37
|
+
await check('write, read, edit, list, find, search a file', true, async () => {
|
|
38
|
+
await boundary.invoke('write', { path: 'src/note.txt', content: 'alpha\nbeta\n' });
|
|
39
|
+
await boundary.invoke('edit', { path: 'src/note.txt', edits: [{ oldText: 'beta', newText: 'gamma' }] });
|
|
40
|
+
if (!/gamma/.test(text(await boundary.invoke('read', { path: 'src/note.txt' })))) throw Error('edit not applied');
|
|
41
|
+
if (!/note\.txt/.test(text(await boundary.invoke('ls', { path: 'src' })))) throw Error('ls did not list the file');
|
|
42
|
+
if (!/note\.txt/.test(text(await boundary.invoke('find', { pattern: '*.txt' })))) throw Error('find did not find the file');
|
|
43
|
+
if (!/note\.txt/.test(text(await boundary.invoke('grep', { pattern: 'gamma' })))) throw Error('grep did not find the text');
|
|
44
|
+
return '6 tools';
|
|
45
|
+
});
|
|
46
|
+
await check('project folder names with spaces and Vietnamese', true, async () => {
|
|
47
|
+
await boundary.invoke('write', { path: 'Dự án mới/ghi chú.md', content: 'ổn\n' });
|
|
48
|
+
if (await bash("cat 'Dự án mới/ghi chú.md'") !== 'ổn') throw Error('content changed'); return 'ok';
|
|
49
|
+
});
|
|
50
|
+
await check('files outside the project stay out of reach', true, async () => {
|
|
51
|
+
let refused = false; try { await boundary.invoke('write', { path: '../piagent-doctor-outside.txt', content: 'x' }); } catch { refused = true; }
|
|
52
|
+
if (!refused) { fs.rmSync(path.join(path.dirname(project), 'piagent-doctor-outside.txt'), { force: true }); throw Error('a write outside the project was allowed'); }
|
|
53
|
+
return 'refused';
|
|
54
|
+
});
|
|
55
|
+
await check('commands have no network unless approved', true, async () => {
|
|
56
|
+
let refused = false; try { await bash('curl -sS -m 5 https://example.com/ >/dev/null'); } catch { refused = true; }
|
|
57
|
+
if (!refused) throw Error('a command reached the internet without approval'); return 'blocked';
|
|
58
|
+
});
|
|
59
|
+
await check('git', true, () => bash('git --version'));
|
|
60
|
+
await check('git commit with the member\'s identity', false, async () => {
|
|
61
|
+
await bash('git init -q . && git add -A');
|
|
62
|
+
try { return await bash('git commit -qm doctor && git log -1 --format="%an <%ae>"'); }
|
|
63
|
+
catch (error) { throw Error(/tell me who you are|user\.(name|email)/i.test(String(error?.message)) ? 'no git identity: run git config --global user.name and user.email' : error?.message); }
|
|
64
|
+
});
|
|
65
|
+
await check('node', true, () => bash('node --version'));
|
|
66
|
+
await check('npm', false, () => bash('npm --version'));
|
|
67
|
+
await check('python3', false, () => bash('python3 --version'));
|
|
68
|
+
await check('C compiler (native npm modules, Rust, cgo)', false, async () => {
|
|
69
|
+
await boundary.invoke('write', { path: 'main.c', content: 'int main(void){return 0;}\n' });
|
|
70
|
+
await bash('cc main.c -o main && ./main'); return first(await bash('cc --version'));
|
|
71
|
+
});
|
|
72
|
+
await check('make', false, () => bash('make --version'));
|
|
73
|
+
if (network) await check('web_fetch reads a public page', false, async () => `status ${(await fetchPublicPage('https://example.com/', { maxChars: 1000 })).status}`);
|
|
74
|
+
} finally { await boundary?.dispose(); fs.rmSync(project, { recursive: true, force: true }); }
|
|
75
|
+
if (broker) {
|
|
76
|
+
let manifest = null;
|
|
77
|
+
await check('Agent Watch answers with the company key', true, async () => {
|
|
78
|
+
manifest = await broker.request('config');
|
|
79
|
+
if (manifest?.schema_version !== 2 || !manifest.harness) throw Error('the key has no harness');
|
|
80
|
+
return `key "${manifest.key_label ?? manifest.key_id}"`;
|
|
81
|
+
});
|
|
82
|
+
if (manifest) await check('this Piagent knows every harness model', true, async () => {
|
|
83
|
+
const api = await import(pathToFileURL(path.join(sdk, 'dist/index.js'))), ai = await import(pathToFileURL(path.join(sdk, 'node_modules/@earendil-works/pi-ai/dist/index.js')));
|
|
84
|
+
const runtime = await api.ModelRuntime.create({ credentials: new ai.InMemoryCredentialStore(), modelsPath: null, allowModelNetwork: false, refreshOnCreate: false });
|
|
85
|
+
const roles = Object.entries(manifest.harness.configuration ?? {}).filter(([, role]) => role?.model_ids?.length);
|
|
86
|
+
for (const [role, value] of roles) for (const id of value.model_ids) {
|
|
87
|
+
const model = (manifest.models ?? []).find(entry => entry.id === id);
|
|
88
|
+
if (!model) throw Error(`${role}: the key is not granted a harness model; ask an administrator to save the harness again`);
|
|
89
|
+
if (!nativeManagedModel(runtime, piProvider(model.owned_by), model.provider_model_id)) throw Error(`${role}: unknown to this Piagent; update Piagent`);
|
|
90
|
+
}
|
|
91
|
+
return `${roles.map(([role]) => role).join(', ')}; thinking ${(manifest.thinking_levels ?? []).join('/') || 'off only'}`;
|
|
92
|
+
});
|
|
93
|
+
if (network) await check('Studio is reachable', true, async () => {
|
|
94
|
+
// Any HTTP answer proves the route; an unauthenticated one is refused.
|
|
95
|
+
const response = await fetch(new URL('/v1/models', origin), { signal: AbortSignal.timeout(8000) });
|
|
96
|
+
return `HTTP ${response.status}`;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function doctorReport(results) {
|
|
103
|
+
const failed = results.filter(result => !result.ok && result.required), missing = results.filter(result => !result.ok && !result.required);
|
|
104
|
+
const lines = results.map(result => `${result.ok ? ' ok ' : result.required ? ' FAIL' : ' miss'} ${result.label}${result.detail ? ` — ${result.detail}` : ''}`);
|
|
105
|
+
lines.push(failed.length ? `Not ready: ${failed.length} required check(s) failed.` : `Ready for company sessions${missing.length ? `; ${missing.length} optional item(s) missing` : ''}. No model request was sent.`);
|
|
106
|
+
return { text: lines.join('\n'), ok: failed.length === 0 };
|
|
107
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { REVIEW_PROMPT, VERIFY_PROMPT, isCheckCommand } from './workflow.mjs';
|
|
2
|
+
|
|
3
|
+
// Each helper role runs at most this often per user message (Studio's MaxHelperCalls).
|
|
4
|
+
export const HELPER_CALLS = 8;
|
|
5
|
+
|
|
6
|
+
// The helpers a main agent may delegate to, in the Harness's order. Each is
|
|
7
|
+
// optional: only those the company Harness enables (and Agent Watch passes
|
|
8
|
+
// on) are offered. Scout reads the code, research also reads the web, verify
|
|
9
|
+
// re-checks a result with the repository's checks and primary sources,
|
|
10
|
+
// review judges the patch. None of them changes files.
|
|
11
|
+
export const HELPER_ROLES = Object.freeze(['scout', 'research', 'verify', 'review']);
|
|
12
|
+
export const helperRoles = manifest => HELPER_ROLES.filter(role => manifest?.harness?.configuration?.[role]);
|
|
13
|
+
const WIDE_READING = 'when understanding the task needs more reading than a few batches of files, such as a flow across many modules of an unfamiliar codebase';
|
|
14
|
+
export function helperPrompt(roles) {
|
|
15
|
+
const lines = [];
|
|
16
|
+
if (roles.includes('scout')) lines.push(`Use the scout helper (delegate role "scout") ${WIDE_READING}, and work from its file:line findings; do small lookups yourself.`);
|
|
17
|
+
if (roles.includes('research')) lines.push(roles.includes('scout') ? 'Use the research helper (delegate role "research") for questions that need external documentation or current information (library and API docs, releases, error messages) together with the code.'
|
|
18
|
+
: `Use the research helper (delegate role "research") ${WIDE_READING} or external documentation, and work from its findings; do small lookups yourself.`);
|
|
19
|
+
if (roles.includes('verify')) lines.push('Before you report a non-trivial result as done, you may have the verify helper (delegate role "verify") confirm it independently: it runs the repository checks on the current code and checks claims against the code and primary sources, answering pass, fail or unverifiable with evidence.');
|
|
20
|
+
return lines.length ? ' ' + lines.join(' ') : '';
|
|
21
|
+
}
|
|
22
|
+
const HELPER_USES = {
|
|
23
|
+
scout: () => 'scout: before changing an area you do not know that spans more files than you can read in a few batches (where something is computed and every caller, how a flow crosses modules). It reads only the project, no web, and answers with file:line facts; your own context stays free for the change.',
|
|
24
|
+
research: roles => roles.includes('scout') ? 'research: questions that need the web as well as the code: library or API documentation, releases, error messages, current information.'
|
|
25
|
+
: 'research: before changing an area you do not know that spans more files than you can read in a few batches (where something is computed and every caller, how a flow crosses modules), or to read external documentation; your own context stays free for the change. It also searches the web.',
|
|
26
|
+
verify: () => 'verify: an independent check of a result: it runs the repository checks on the current code (offline, and the project is read-only for its commands) and confirms claims, yours or a helper\'s, against the code and primary sources. It answers pass, fail or unverifiable per claim, with evidence.',
|
|
27
|
+
review: () => 'review: an independent review of the current patch.',
|
|
28
|
+
};
|
|
29
|
+
export function delegateDescription(roles) {
|
|
30
|
+
return `Hand work to a helper with its own context. Helpers read the project but cannot change files or call helpers. ${roles.map(role => HELPER_USES[role](roles)).join(' ')} A lookup that one grep or read answers stays with you. Write the task as a brief the helper can act on alone: the goal, what to find or check, where to start and what you already know or ruled out, and the answer you need (file:line facts). One call per helper runs at a time; each can be called again, up to ${HELPER_CALLS} times per user message, for example a second review after fixing what the first one found.`;
|
|
31
|
+
}
|
|
32
|
+
const RESEARCH_PROMPT = 'Research the assigned question. Read only the needed source; use web_search or web_fetch for external documentation. Answer with facts the main agent can act on: file:line, short quotes, what calls what, and what you checked and found nothing in. You cannot change files or delegate.';
|
|
33
|
+
const SCOUT_PROMPT = 'Scout the codebase for the assigned question. Search first (grep, find), then read only the parts that matter. Answer with facts the main agent can act on: file:line, short quotes, what calls what, and where you looked and found nothing. You have no web access: when the answer needs external documentation, say so. You cannot change files or delegate.';
|
|
34
|
+
export const READ_TOOLS = ['read', 'grep', 'find', 'ls'];
|
|
35
|
+
// What each helper may use. Every helper's project is read-only; verify also
|
|
36
|
+
// runs commands there (offline, writes only to its temporary directory).
|
|
37
|
+
export const HELPER_SETUP = {
|
|
38
|
+
scout: { web: false, commands: false, prompt: SCOUT_PROMPT, label: 'Scout' },
|
|
39
|
+
research: { web: true, commands: false, prompt: RESEARCH_PROMPT, label: 'Research' },
|
|
40
|
+
verify: { web: true, commands: true, prompt: VERIFY_PROMPT, label: 'Verify' },
|
|
41
|
+
review: { web: false, commands: false, prompt: REVIEW_PROMPT, label: 'Review' },
|
|
42
|
+
};
|
|
43
|
+
// A repository check a helper ran counts for the run like one the main
|
|
44
|
+
// agent ran, when the code was the same before and after it: the helper
|
|
45
|
+
// cannot write, but the main agent may edit beside it in the same answer.
|
|
46
|
+
export function countedCheck(managed, tool) {
|
|
47
|
+
if (tool.name !== 'bash') return tool;
|
|
48
|
+
return { ...tool, execute: async (...args) => {
|
|
49
|
+
const counted = managed.run && isCheckCommand(args[1]?.command, managed.checks.commands), before = counted ? await managed.digest() : null;
|
|
50
|
+
let ok = false;
|
|
51
|
+
try { const result = await tool.execute(...args); ok = true; return result; }
|
|
52
|
+
finally {
|
|
53
|
+
const after = counted && before !== null ? await managed.digest() : null;
|
|
54
|
+
if (after !== null && after === before) await managed.run.afterTool('bash', args[1], ok, async () => after);
|
|
55
|
+
}
|
|
56
|
+
} };
|
|
57
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Reviewed gaps in Pi 0.87.1's bundled catalog. Never infer context/output
|
|
2
|
+
// limits for an unknown future model.
|
|
3
|
+
// Claude Sonnet 5.5, checked 2026-09-30:
|
|
4
|
+
// https://platform.claude.com/docs/en/models/overview
|
|
5
|
+
// https://platform.claude.com/docs/en/build-with-claude/effort
|
|
6
|
+
const sonnet55 = {
|
|
7
|
+
id: 'claude-sonnet-5-5', provider: 'anthropic', api: 'anthropic-messages',
|
|
8
|
+
name: 'Claude Sonnet 5.5', reasoning: true, input: ['text', 'image'],
|
|
9
|
+
contextWindow: 1000000, maxTokens: 128000,
|
|
10
|
+
cost: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
|
|
11
|
+
thinkingLevelMap: { off: null, minimal: null, low: 'low', medium: 'medium', high: 'high', xhigh: 'xhigh', max: 'max' },
|
|
12
|
+
compat: { forceAdaptiveThinking: true, supportsTemperature: false, supportsStrictTools: true, supportsMidConvoEffort: true },
|
|
13
|
+
promptCache: { short: 300, long: 3600 },
|
|
14
|
+
};
|
|
15
|
+
// GPT-6.1 Sol through Codex, checked 2026-10-01: Codex keeps the 272k window
|
|
16
|
+
// of its pricing tier (the API model has 1.05M); 128k output; efforts low to
|
|
17
|
+
// max, no none/minimal; $2/$10 per 1M, cached $0.10, doubled above 272k.
|
|
18
|
+
// https://developers.openai.com/api/docs/models/gpt-6.1-sol
|
|
19
|
+
// https://learn.chatgpt.com/docs/models
|
|
20
|
+
// https://pi.dev/models/openai-codex/gpt-6-1-sol
|
|
21
|
+
const sol61 = {
|
|
22
|
+
id: 'gpt-6.1-sol', provider: 'openai-codex', api: 'openai-codex-responses', baseUrl: 'https://chatgpt.com/backend-api',
|
|
23
|
+
name: 'GPT-6.1 Sol', reasoning: true, input: ['text', 'image'],
|
|
24
|
+
contextWindow: 272000, maxTokens: 128000,
|
|
25
|
+
cost: { input: 2, output: 10, cacheRead: 0.1, cacheWrite: 2.5, tiers: [{ inputTokensAbove: 272000, input: 4, output: 15, cacheRead: 0.2, cacheWrite: 5 }] },
|
|
26
|
+
thinkingLevelMap: { off: null, minimal: 'low', low: 'low', medium: 'medium', high: 'high', xhigh: 'xhigh', max: 'max' },
|
|
27
|
+
compat: { supportsOpenAIGrammarTools: true, supportsAdditionalTools: true, supportsToolSearch: true, supportsMidConvoSystemMessages: true },
|
|
28
|
+
inputLimits: { images: { resize: { maxWidth: 2000, maxHeight: 2000, maxBytes: 4718592, jpegQuality: 80 } } },
|
|
29
|
+
};
|
|
30
|
+
const GAPS = { anthropic: [sonnet55], 'openai-codex': [sol61] };
|
|
31
|
+
// Studio's providers: the Claude and Codex subscriptions, or an API-key
|
|
32
|
+
// vendor (DeepSeek, Kimi, GLM, MiMo, Qwen, OpenCode, Grok) whose id is Pi's
|
|
33
|
+
// own provider id; Studio runs vendors as OpenAI Chat Completions.
|
|
34
|
+
const VENDOR = /^[a-z][a-z0-9-]{1,39}$/;
|
|
35
|
+
export const isVendor = provider => typeof provider === 'string' && provider !== 'claude' && provider !== 'codex' && VENDOR.test(provider);
|
|
36
|
+
export function piProvider(provider) { return provider === 'claude' ? 'anthropic' : provider === 'codex' ? 'openai-codex' : provider; }
|
|
37
|
+
export function studioAPI(provider) { return provider === 'claude' ? 'anthropic-messages' : provider === 'codex' ? 'openai-responses' : 'openai-completions'; }
|
|
38
|
+
export function studioPath(provider) { return provider === 'claude' ? '/claude' : provider === 'codex' ? '/v1' : '/openai/v1'; }
|
|
39
|
+
export function nativeManagedModel(runtime, provider, id) {
|
|
40
|
+
const gap = GAPS[provider]?.find(model => model.id === id);
|
|
41
|
+
return runtime.getModel(provider, id) ?? (gap ? structuredClone(gap) : undefined);
|
|
42
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
const quote = value => "'" + value.replaceAll("'", "'\"'\"'") + "'";
|
|
5
|
+
const textContent = result => result.content?.filter(p => p.type === 'text').map(p => p.text).join('\n') ?? '';
|
|
6
|
+
|
|
7
|
+
// The patch a reviewer sees (diff against HEAD plus untracked files), read
|
|
8
|
+
// inside the tool boundary. It comes in parts: the shell tool truncates its
|
|
9
|
+
// output well below the 120 KB a review may cover. Every part names the
|
|
10
|
+
// snapshot's digest, so parts of a patch that changed meanwhile never mix.
|
|
11
|
+
export async function readPatchSnapshot(boundary) {
|
|
12
|
+
const worker = fileURLToPath(new URL('./review-snapshot-worker.mjs', import.meta.url));
|
|
13
|
+
const read = async part => {
|
|
14
|
+
const result = await boundary.invoke('bash', { command: `${quote(process.execPath)} ${quote(worker)} ${part}`, timeout: 20 });
|
|
15
|
+
if (result.details?.truncation?.truncated) throw Error('managed-review-patch-too-large');
|
|
16
|
+
const text = textContent(result).trim(), newline = text.indexOf('\n');
|
|
17
|
+
const [digest, parts] = (newline < 0 ? text : text.slice(0, newline)).split(' ');
|
|
18
|
+
if (!/^[a-f0-9]{64}$/.test(digest ?? '') || !(Number(parts) >= 1 && Number(parts) <= 8)) throw Error('managed-review-patch-unreadable');
|
|
19
|
+
return { digest, parts: Number(parts), bytes: Buffer.from(newline < 0 ? '' : text.slice(newline + 1), 'base64') };
|
|
20
|
+
};
|
|
21
|
+
const first = await read(0), chunks = [first.bytes];
|
|
22
|
+
for (let part = 1; part < first.parts; part += 1) {
|
|
23
|
+
const next = await read(part);
|
|
24
|
+
if (next.digest !== first.digest) throw Error('managed-review-patch-changed');
|
|
25
|
+
chunks.push(next.bytes);
|
|
26
|
+
}
|
|
27
|
+
const patch = Buffer.concat(chunks).toString('utf8');
|
|
28
|
+
if (createHash('sha256').update(patch).digest('hex') !== first.digest) throw Error('managed-review-patch-changed');
|
|
29
|
+
return { patch, digest: first.digest };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Why a required review could not run, in one word for the member.
|
|
33
|
+
export function reviewUnavailableReason(message) {
|
|
34
|
+
const text = String(message ?? '');
|
|
35
|
+
if (/review-(patch|file|untracked)-limit|managed-review-patch-too-large/.test(text)) return 'too_large';
|
|
36
|
+
if (/not a git repository|Could not access 'HEAD'|managed-review-patch-unreadable/i.test(text)) return 'not_git';
|
|
37
|
+
if (/managed-helper-limit|helper_run_limit_reached/.test(text)) return 'limit';
|
|
38
|
+
return 'failed';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The patch as a reviewer reads it: the diff, then each new file in full.
|
|
42
|
+
export function reviewText(snapshot) {
|
|
43
|
+
let value;
|
|
44
|
+
try { value = JSON.parse(snapshot.patch); } catch { return `Patch snapshot ${snapshot.digest}:\n${snapshot.patch}`; }
|
|
45
|
+
const files = (value.untracked ?? []).map(f => f.mode === 'symlink' ? `=== new symlink ${f.path} -> ${f.target}`
|
|
46
|
+
: f.encoding === 'utf8' ? `=== new file ${f.path}\n${f.contents}` : `=== new binary file ${f.path} (${Math.round(f.contents.length * 0.75)} bytes)`);
|
|
47
|
+
return `Patch snapshot ${snapshot.digest}:\n--- diff against HEAD ---\n${value.patch || '(no changes to tracked files)'}\n${files.length ? `--- new files ---\n${files.join('\n\n')}` : ''}`;
|
|
48
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {spawn} from 'node:child_process';
|
|
5
|
+
import {createHash} from 'node:crypto';
|
|
6
|
+
import {managedGit} from './toolchain.mjs';
|
|
7
|
+
|
|
8
|
+
const quote = value => "'" + value.replaceAll("'", "'\"'\"'") + "'";
|
|
9
|
+
const text = result => result.content.filter(p=>p.type==='text').map(p=>p.text).join('\n').trim();
|
|
10
|
+
const hash = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
11
|
+
|
|
12
|
+
export function githubRemote(value) {
|
|
13
|
+
// No arbitrary protocol helper, userinfo, port, query, refspec or SSH command.
|
|
14
|
+
const match = /^(?:https:\/\/github\.com\/|git@github\.com:)([A-Za-z0-9][A-Za-z0-9_.-]{0,99})\/([A-Za-z0-9][A-Za-z0-9_.-]{0,99}?)(?:\.git)?$/.exec(value);
|
|
15
|
+
if (!match || match.slice(1).some(x=>x==='.'||x==='..')) throw Error('managed-repository-origin-unsupported');
|
|
16
|
+
return {repository:`${match[1]}/${match[2]}`,url:`https://github.com/${match[1]}/${match[2]}.git`};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function repositoryFetchPlan(boundary) {
|
|
20
|
+
const result=await boundary.invoke('bash',{command:'git config --local --get remote.origin.url',timeout:5});
|
|
21
|
+
const remote=githubRemote(text(result));
|
|
22
|
+
return Object.freeze({...remote,cwd:boundary.cwd});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function command(binary,args,{cwd,env,signal,secret='',limit=1024*1024}={}) {
|
|
26
|
+
return new Promise((resolve,reject)=>{
|
|
27
|
+
if(signal?.aborted) return reject(Error('managed-operation-cancelled'));
|
|
28
|
+
const child=spawn(binary,args,{cwd,env,stdio:['ignore','pipe','pipe']});let output='',size=0,failed=false;
|
|
29
|
+
const abort=()=>{failed=true;child.kill('SIGKILL')};signal?.addEventListener('abort',abort,{once:true});
|
|
30
|
+
const timer=setTimeout(abort,60000);timer.unref();
|
|
31
|
+
child.stdout.on('data',chunk=>{size+=chunk.length;if(size>limit)abort();else output+=chunk});
|
|
32
|
+
// Credential helpers and Git diagnostics are never relayed to model context.
|
|
33
|
+
child.stderr.on('data',()=>{});child.on('error',()=>{failed=true});
|
|
34
|
+
child.on('close',code=>{clearTimeout(timer);signal?.removeEventListener('abort',abort);
|
|
35
|
+
if(failed||code!==0) reject(Error(signal?.aborted?'managed-operation-cancelled':'managed-repository-fetch-failed'));
|
|
36
|
+
else resolve(secret?output.replaceAll(secret,'[REDACTED]'):output);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function fetchRepositoryBundle(plan, destination, signal) {
|
|
42
|
+
// This private process sees only fixed arguments and an empty bare repo. It
|
|
43
|
+
// never executes a command, hook, helper, filter or config from the project.
|
|
44
|
+
const git=managedGit(), gitHash=hash(git);
|
|
45
|
+
const temporary=fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(),'managed-fetch-')));
|
|
46
|
+
fs.chmodSync(temporary,0o700);
|
|
47
|
+
const env={PATH:'/usr/bin:/bin',HOME:temporary,LANG:'en_US.UTF-8',GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:'/dev/null',GIT_TERMINAL_PROMPT:'0',GIT_ASKPASS:'/usr/bin/false'};
|
|
48
|
+
let token='';
|
|
49
|
+
try {
|
|
50
|
+
// gh reads the logged-in user's own GitHub credential, never a Studio key.
|
|
51
|
+
// A public repository works without gh. Private access remains unavailable
|
|
52
|
+
// until that user signs in; no personal credential is put in agent env.
|
|
53
|
+
for(const candidate of ['/opt/homebrew/bin/gh','/usr/local/bin/gh']) {
|
|
54
|
+
if(!fs.existsSync(candidate))continue;
|
|
55
|
+
const gh=fs.realpathSync(candidate);
|
|
56
|
+
try {token=(await command(gh,['auth','token','--hostname','github.com'],{cwd:temporary,env:{PATH:'/usr/bin:/bin',HOME:os.homedir(),LANG:'en_US.UTF-8'},signal,limit:4096})).trim();} catch {}
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
if(token && !/^[A-Za-z0-9_]{10,2048}$/.test(token)) throw Error('managed-github-credential-invalid');
|
|
60
|
+
await command(git,['init','--bare','--quiet',temporary],{cwd:temporary,env,signal});
|
|
61
|
+
if(hash(git)!==gitHash)throw Error('managed-runtime-changed');
|
|
62
|
+
const authentication=token?{GIT_CONFIG_COUNT:'1',GIT_CONFIG_KEY_0:'http.https://github.com/.extraheader',GIT_CONFIG_VALUE_0:'Authorization: Basic '+Buffer.from('x-access-token:'+token).toString('base64')}:{};
|
|
63
|
+
await command(git,['-c','credential.helper=','-c','core.hooksPath=/dev/null','-c','http.followRedirects=false','-c','protocol.allow=never','-c','protocol.https.allow=always','fetch','--quiet','--no-tags','--no-recurse-submodules','--depth=100',plan.url,'+refs/heads/*:refs/heads/*'],{cwd:temporary,env:{...env,...authentication},signal,secret:token});
|
|
64
|
+
// A self-contained bundle cannot be shallow. Do not silently fetch an
|
|
65
|
+
// unbounded history; export fetched objects as a local bare repository.
|
|
66
|
+
fs.cpSync(temporary,destination,{recursive:true,errorOnExist:true,force:false});
|
|
67
|
+
// Remote URL/config is not needed by the untrusted offline import.
|
|
68
|
+
fs.writeFileSync(path.join(destination,'config'),'[core]\n\tbare = true\n');
|
|
69
|
+
} finally {token='';fs.rmSync(temporary,{recursive:true,force:true});}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function executeRepositoryFetch(boundary,plan,signal,{transport=fetchRepositoryBundle}={}) {
|
|
73
|
+
const current=await repositoryFetchPlan(boundary);
|
|
74
|
+
if(current.url!==plan.url||current.cwd!==plan.cwd)throw Error('managed-operation-scope-changed');
|
|
75
|
+
const staging=path.join(boundary.temporary,'approved-origin-'+Date.now());
|
|
76
|
+
try {
|
|
77
|
+
await transport(plan,staging,signal);
|
|
78
|
+
const rechecked=await repositoryFetchPlan(boundary);
|
|
79
|
+
if(rechecked.url!==plan.url)throw Error('managed-operation-scope-changed');
|
|
80
|
+
// Import is sandboxed. Malicious repo hooks/config cannot reach credentials.
|
|
81
|
+
await boundary.invoke('bash',{command:`git -c core.hooksPath=/dev/null -c protocol.allow=never -c protocol.file.allow=always fetch --no-tags --no-recurse-submodules --no-write-fetch-head --update-shallow ${quote(staging)} '+refs/heads/*:refs/remotes/origin/*'`,timeout:30},signal);
|
|
82
|
+
return {content:[{type:'text',text:`Fetched origin branches for ${plan.repository}. Working files and current branch were not changed.`}],details:{operation:'git.fetch',repository:plan.repository}};
|
|
83
|
+
} finally {fs.rmSync(staging,{recursive:true,force:true});}
|
|
84
|
+
}
|