@piagent/platform 1.36.0 → 1.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +1 -1
  3. package/README.vi.md +1 -1
  4. package/docs/command-reference-vietnamese.md +2 -2
  5. package/docs/distribution-standard.md +6 -6
  6. package/docs/languages.json +2 -2
  7. package/docs/operator-manual-vietnamese.md +7 -7
  8. package/docs/publishing-for-teams.md +2 -0
  9. package/docs/release-install-policy.md +2 -2
  10. package/docs/team-onboarding.md +2 -2
  11. package/docs-site/architecture.html +1 -1
  12. package/docs-site/benchmark.html +1 -1
  13. package/docs-site/commands.html +1 -1
  14. package/docs-site/content/en/index.html +4 -4
  15. package/docs-site/content/en/whats-new.html +12 -0
  16. package/docs-site/content/index.html +4 -4
  17. package/docs-site/content/whats-new.html +12 -0
  18. package/docs-site/context-engine.html +1 -1
  19. package/docs-site/ecosystem.html +1 -1
  20. package/docs-site/en/architecture.html +1 -1
  21. package/docs-site/en/benchmark.html +1 -1
  22. package/docs-site/en/commands.html +1 -1
  23. package/docs-site/en/context-engine.html +1 -1
  24. package/docs-site/en/ecosystem.html +1 -1
  25. package/docs-site/en/index.html +5 -5
  26. package/docs-site/en/mcp-approval.html +1 -1
  27. package/docs-site/en/mcp-auth.html +1 -1
  28. package/docs-site/en/mcp-servers.html +1 -1
  29. package/docs-site/en/mcp.html +1 -1
  30. package/docs-site/en/permissions.html +1 -1
  31. package/docs-site/en/profiles.html +1 -1
  32. package/docs-site/en/quickstart.html +1 -1
  33. package/docs-site/en/runtime.html +1 -1
  34. package/docs-site/en/scope.html +1 -1
  35. package/docs-site/en/security.html +1 -1
  36. package/docs-site/en/team.html +1 -1
  37. package/docs-site/en/whats-new.html +13 -1
  38. package/docs-site/en/windows.html +1 -1
  39. package/docs-site/en/workflows.html +1 -1
  40. package/docs-site/index.html +5 -5
  41. package/docs-site/mcp-approval.html +1 -1
  42. package/docs-site/mcp-auth.html +1 -1
  43. package/docs-site/mcp-servers.html +1 -1
  44. package/docs-site/mcp.html +1 -1
  45. package/docs-site/permissions.html +1 -1
  46. package/docs-site/profiles.html +1 -1
  47. package/docs-site/quickstart.html +1 -1
  48. package/docs-site/runtime.html +1 -1
  49. package/docs-site/scope.html +1 -1
  50. package/docs-site/security.html +1 -1
  51. package/docs-site/team.html +1 -1
  52. package/docs-site/whats-new.html +13 -1
  53. package/docs-site/windows.html +1 -1
  54. package/docs-site/workflows.html +1 -1
  55. package/evals/long-task-soak-v1/README.md +96 -0
  56. package/evals/long-task-soak-v1/lib/judge.mjs +67 -0
  57. package/evals/long-task-soak-v1/lib/persona.mjs +63 -0
  58. package/evals/long-task-soak-v1/lib/report.mjs +35 -0
  59. package/evals/long-task-soak-v1/lib/session-loop.mjs +165 -0
  60. package/evals/long-task-soak-v1/lib/workspace.mjs +115 -0
  61. package/evals/long-task-soak-v1/runner.mjs +89 -0
  62. package/evals/long-task-soak-v1/scenarios/autonomous-node-inventory.json +15 -0
  63. package/evals/long-task-soak-v1/scenarios/business-python-billing.json +34 -0
  64. package/evals/long-task-soak-v1/scenarios/framework-go-docker.json +40 -0
  65. package/evals/long-task-soak-v1/scenarios/framework-playwright-e2e.json +15 -0
  66. package/evals/long-task-soak-v1/scenarios/harness-guardrails-secrets.json +23 -0
  67. package/evals/long-task-soak-v1/scenarios/long-horizon-polyglot-rename.json +30 -0
  68. package/evals/long-task-soak-v1/scenarios/long-node-inventory-platform.json +64 -0
  69. package/evals/long-task-soak-v1/scenarios/long-polyglot-platform.json +288 -0
  70. package/evals/long-task-soak-v1/scenarios/long-postgres-booking-platform.json +210 -0
  71. package/evals/long-task-soak-v1/scenarios/long-python-billing-platform.json +84 -0
  72. package/evals/long-task-soak-v1/scenarios/mixed-requirement-change.json +20 -0
  73. package/evals/long-task-soak-v1/scenarios/stability-flaky-slow-suite.json +22 -0
  74. package/evals/long-task-soak-v1/scenarios/thirdparty-postgres-integration.json +30 -0
  75. package/evals/long-task-soak-v1/surfaces/terminal-rpc.mjs +150 -0
  76. package/evals/long-task-soak-v1/surfaces/webui-gateway.mjs +214 -0
  77. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  78. package/package.json +2 -1
  79. package/packages/piagent-core/managed/session.mjs +3 -3
  80. package/packages/piagent-core/managed/studio-link.mjs +34 -4
  81. package/packages/piagent-core/package.json +1 -1
  82. package/packages/piagent-core/runtime/managed-failure.mjs +5 -0
  83. package/scripts/company-terminal.mjs +2 -1
  84. package/scripts/piagent-studio.mjs +13 -3
@@ -15,6 +15,7 @@ import path from 'node:path';
15
15
  import { randomUUID } from 'node:crypto';
16
16
  import { setTimeout as sleep } from 'node:timers/promises';
17
17
  import { CLIENT_AGENT } from './client-agent.mjs';
18
+ import { failureCode, failureText } from '../runtime/managed-failure.mjs';
18
19
 
19
20
  // Codes whose cause is Studio or the way to it: Studio, its gateway or its
20
21
  // connector did not answer (stopped, restarting, an update), or Agent Watch
@@ -34,6 +35,13 @@ const LOG_BYTES = 2 * 1024 * 1024, QUEUE = 50, BATCH = 20, SEND_TRIES = 5;
34
35
  const MIN_PAUSE_MS = 2_000, MAX_PAUSE_MS = 30_000, MAX_STREAK_PAUSE_MS = 5 * 60_000, BEAT_TIMEOUT_MS = 5_000;
35
36
  // After Studio was away, Agent Watch gets this long to reach it again.
36
37
  export const BROKER_GRACE_MS = 10 * 60_000;
38
+ // Agent Watch could not reach Studio while Studio answers here: its way to
39
+ // Studio failed for a moment (a gateway's 5xx, a dropped connection, a slow
40
+ // answer). Found on 2026-10-22: one such failure renewing the grant stopped a
41
+ // 25-minute task at once. It is asked again, pausing longer each time, until
42
+ // it has failed for this long without a request going through; then it fails
43
+ // as before (Agent Watch itself is broken, not the way to Studio).
44
+ export const BROKER_RETRY_MS = 3 * 60_000;
37
45
 
38
46
  const header = (headers, name) => {
39
47
  if (!headers) return null;
@@ -74,7 +82,7 @@ export class StudioLink {
74
82
  if (seen.boot) this.last = { boot: seen.boot, version: seen.version ?? this.last.version };
75
83
  }
76
84
  // A request went through: the next wait starts with a short pause again.
77
- ok() { this.streak = 0; }
85
+ ok() { this.streak = 0; this.brokerSince = null; }
78
86
  waits(code) { return STUDIO_WAIT_CODES.has(code); }
79
87
  // Studio or the way to it, whatever Studio's heartbeat says now.
80
88
  always(code) { return this.waits(code) && !conditional(code); }
@@ -107,7 +115,7 @@ export class StudioLink {
107
115
  if (signal?.aborted) throw signal.reason ?? Error('aborted');
108
116
  if (!this.waits(code)) return null;
109
117
  const first = conditional(code) ? await this.beat(signal) : null;
110
- if (first?.state === 'ready' && !this.justBack()) return null;
118
+ if (first?.state === 'ready' && !this.justBack() && !this.brokerRetry(code)) return null;
111
119
  const controller = new AbortController(), stop = () => controller.abort(signal?.reason);
112
120
  signal?.addEventListener('abort', stop, { once: true });
113
121
  this.waiting.add(controller);
@@ -115,7 +123,8 @@ export class StudioLink {
115
123
  const seen = new Set(first ? [first.state] : []);
116
124
  this.log({ kind: 'studio_wait', state: 'start', id: event.id, phase, role, code, version: event.before.version, conversation });
117
125
  this.notify({ type: 'managed_studio_wait', id: event.id, role, phase, code, state: 'start', waitedMs: 0 });
118
- this.status(`Studio is not answering (${code}). Waiting; the work goes on by itself when Studio is back. Esc stops.`);
126
+ this.status(first?.state === 'ready' ? `Agent Watch could not reach Studio (${code}). Asking again; the work goes on by itself. Esc stops.`
127
+ : `Studio is not answering (${code}). Waiting; the work goes on by itself when Studio is back. Esc stops.`);
119
128
  // Failing again right after a wait (Studio answers, the request does
120
129
  // not): each such wait starts with a longer pause, up to five minutes.
121
130
  let pause = Math.min(MAX_STREAK_PAUSE_MS, this.minPauseMs * 2 ** Math.min(this.streak, 8));
@@ -147,7 +156,15 @@ export class StudioLink {
147
156
  async needed(code, signal) {
148
157
  if (signal?.aborted) throw signal.reason ?? Error('aborted');
149
158
  if (!this.waits(code)) return false;
150
- return !(conditional(code) && (await this.beat(signal)).state === 'ready' && !this.justBack());
159
+ return !(conditional(code) && (await this.beat(signal)).state === 'ready' && !this.justBack() && !this.brokerRetry(code, false));
160
+ }
161
+ // An Agent Watch failure on a Studio that answers: asked again within
162
+ // BROKER_RETRY_MS of the first one since a request last went through.
163
+ brokerRetry(code, start = true) {
164
+ if (!BROKER.test(code)) return false;
165
+ const since = this.brokerSince ?? this.now();
166
+ if (start) this.brokerSince = since;
167
+ return this.now() - since < (this.brokerRetryMs ?? BROKER_RETRY_MS);
151
168
  }
152
169
  justBack() { return !!this.recoveredAt && this.now() - this.recoveredAt < (this.brokerGraceMs ?? BROKER_GRACE_MS); }
153
170
 
@@ -239,3 +256,16 @@ export function resumeMessage(wait, code) {
239
256
  content: `Harness: your last step was cut off because ${why} [${code}]; Studio answers again after about ${minutes} min. Nothing you asked for is lost: the files and the checklist are as you left them. Check where you were (the checklist, git status, the last command you ran) and continue the member's task from there; do not redo work that is already done.`,
240
257
  details: { phase: 'resume', cause: wait.cause, waitedMs: wait.waited_ms, code } };
241
258
  }
259
+
260
+ // A broker call (closing the round's run) when Agent Watch's way to Studio
261
+ // fails for a moment: asked again while the link waits, so a passing gateway
262
+ // error does not fail a turn whose work is done.
263
+ export async function viaLink(link, ask, { conversation = null, runId = null } = {}) {
264
+ for (;;) {
265
+ try { return await ask(); }
266
+ catch (error) {
267
+ const code = failureCode(failureText(error));
268
+ if (!(await link?.waitUntilBack({ phase: 'broker', code, conversation, runId }).catch(() => null))) throw error;
269
+ }
270
+ }
271
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/core",
3
- "version": "1.36.0",
3
+ "version": "1.37.0",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "license": "MIT",
@@ -166,6 +166,11 @@ export function failureCode(raw) {
166
166
  // The provider library's words for an answer stream that ended empty: Studio
167
167
  // admitted the request and nothing came back. It is not asked again.
168
168
  if (/stream ended without a (terminal event|stop reason)|response has no body/i.test(text)) return 'upstream_incomplete';
169
+ // Studio's own words when its upstream cut an answer already streaming: its
170
+ // error event carries the code, which the provider libraries drop (seen
171
+ // 2026-10-21: "Request interrupted" stopped a long task with no ask again).
172
+ if (/^Request interrupted\b/i.test(text)) return 'upstream_interrupted';
173
+ if (/^The answer ran past its output limit\b/i.test(text)) return 'output_limit_exceeded';
169
174
  if (NETWORK.test(text)) return 'studio-unreachable';
170
175
  return 'managed-request-failed';
171
176
  }
@@ -388,7 +388,8 @@ export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindin
388
388
  const session = managed()?.session;
389
389
  if (!session?.subscribe || heard === session) return;
390
390
  heard = session;
391
- const showBranch = async () => { const branch = await branchOf(managed()?.cwd ?? ctx.cwd); ctx.ui?.setStatus?.('agent-watch-branch', branch ? `⎇ ${branch}` : undefined); };
391
+ // The conversation can close while git answers (a client ends right after a turn): its ctx is then stale.
392
+ const showBranch = async () => { try { const branch = await branchOf(managed()?.cwd ?? ctx.cwd); ctx.ui?.setStatus?.('agent-watch-branch', branch ? `⎇ ${branch}` : undefined); } catch { /* closed meanwhile */ } };
392
393
  ctx.ui?.setWorkingMessage?.(WORKING);
393
394
  void showBranch();
394
395
  session.subscribe((event) => {
@@ -15,16 +15,17 @@ const digest = file => createHash('sha256').update(fs.readFileSync(file)).digest
15
15
  process.removeAllListeners('warning');
16
16
  process.on('warning', warning => { if (warning.name !== 'ExperimentalWarning') process.stderr.write(`${warning.name}: ${warning.message}\n`); });
17
17
  const args = process.argv.slice(2), flags = new Map();
18
- if (args.includes('--help')) { console.log('piagent studio [--web [--no-open] | --serve | --check | --doctor | --prompt <text> [--json]] [--config <Agent Watch configuration>] [--project <folder>]\nDefaults: the configuration imported into ~/.pi/agent and the current project. Personal sessions remain under pi / piagent dashboard.\n--json with --prompt writes the event stream of `pi --mode json` (JSONL on stdout; failures on stderr, exit 1).'); process.exit(0); }
18
+ if (args.includes('--help')) { console.log('piagent studio [--web [--no-open] | --serve | --check | --doctor | --prompt <text> [--json] | --rpc] [--config <Agent Watch configuration>] [--project <folder>]\nDefaults: the configuration imported into ~/.pi/agent and the current project. Personal sessions remain under pi / piagent dashboard.\n--json with --prompt writes the event stream of `pi --mode json` (JSONL on stdout; failures on stderr, exit 1).\n--rpc speaks the protocol of `pi --mode rpc` on stdin/stdout for one company conversation (several messages, questions as extension UI requests).'); process.exit(0); }
19
19
  for (let i = 0; i < args.length; i++) {
20
- if (['--check', '--doctor', '--web', '--serve', '--no-open', '--json'].includes(args[i])) { flags.set(args[i], true); continue; }
20
+ if (['--check', '--doctor', '--web', '--serve', '--no-open', '--json', '--rpc'].includes(args[i])) { flags.set(args[i], true); continue; }
21
21
  if (!['--config', '--project', '--prompt'].includes(args[i]) || flags.has(args[i]) || !args[i + 1]) throw Error('managed-launch-arguments');
22
22
  flags.set(args[i], args[++i]);
23
23
  }
24
24
  if (flags.has('--json') && !flags.has('--prompt')) throw Error('managed-launch-arguments');
25
+ if (flags.has('--rpc') && ['--check', '--doctor', '--web', '--serve', '--prompt', '--json'].some((f) => flags.has(f))) throw Error('managed-launch-arguments');
25
26
  const home = os.homedir();
26
27
  // Joining Agent Watch takes a few seconds; keys typed before the Terminal draws are lost, so say so.
27
- if (!['--check', '--prompt', '--serve', '--doctor', '--web'].some((f) => flags.has(f)) && process.stderr.isTTY) process.stderr.write('Piagent công ty: đang kết nối Agent Watch, chờ ô nhập hiện ra rồi gõ…\n');
28
+ if (!['--check', '--prompt', '--serve', '--doctor', '--web', '--rpc'].some((f) => flags.has(f)) && process.stderr.isTTY) process.stderr.write('Piagent công ty: đang kết nối Agent Watch, chờ ô nhập hiện ra rồi gõ…\n');
28
29
  // Do this before importing the SDK: it must not discover personal keys, Node
29
30
  // preload hooks, proxy overrides or provider endpoints from an ambient shell.
30
31
  const clean = { PATH: '/usr/bin:/bin', HOME: home, LANG: 'en_US.UTF-8', TERM: process.env.TERM || 'xterm-256color', ...interopEnvironment() };
@@ -182,6 +183,15 @@ try {
182
183
  const { failureLines } = await import('./company-terminal.mjs');
183
184
  process.stderr.write(`${failureLines(last.errorMessage) ?? String(last.errorMessage??'Agent Watch: the request failed [managed-request-failed]')}\n`);process.exitCode = 1;
184
185
  }
186
+ } else if (flags.has('--rpc')) {
187
+ // One company conversation driven by a client (a script, a test that
188
+ // plays the member) over Pi's RPC protocol. Pi exits the process when the
189
+ // client closes stdin, so the conversation, its broker and its lease are
190
+ // released inside Pi's own dispose instead of the finally below.
191
+ process.env.PI_OFFLINE = '1'; process.env.PI_SKIP_VERSION_CHECK = '1';
192
+ const dispose = host.dispose.bind(host);
193
+ host.dispose = async () => { try { await dispose(); } finally { await current?.dispose(); current = null; hold?.release(); } };
194
+ await api.runRpcMode(host);
185
195
  } else {
186
196
  // The company terminal runs the pinned Pi release: no "Run pi update" notice
187
197
  // (updating Pi breaks the Agent Watch import), no package-update, catalog or