@piagent/platform 1.35.0 → 1.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +1 -1
- package/README.vi.md +1 -1
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/distribution-standard.md +6 -6
- package/docs/languages.json +2 -2
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +2 -0
- package/docs/release-install-policy.md +2 -2
- package/docs/team-onboarding.md +2 -2
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/en/index.html +4 -4
- package/docs-site/content/en/whats-new.html +12 -0
- package/docs-site/content/index.html +4 -4
- package/docs-site/content/whats-new.html +12 -0
- package/docs-site/context-engine.html +1 -1
- package/docs-site/ecosystem.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/ecosystem.html +1 -1
- package/docs-site/en/index.html +5 -5
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +1 -1
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/whats-new.html +13 -1
- package/docs-site/en/windows.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +5 -5
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/whats-new.html +13 -1
- package/docs-site/windows.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
- package/package.json +1 -1
- package/packages/piagent-core/extensions/repository-memory.js +27 -2
- package/packages/piagent-core/managed/continuation.mjs +23 -2
- package/packages/piagent-core/managed/memory-lessons.mjs +34 -6
- package/packages/piagent-core/managed/memory-serves.mjs +17 -0
- package/packages/piagent-core/managed/memory.mjs +12 -10
- package/packages/piagent-core/managed/request-stream.mjs +8 -2
- package/packages/piagent-core/managed/skill-feedback.mjs +79 -0
- package/packages/piagent-core/managed/studio-link.mjs +18 -0
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/managed-failure.mjs +8 -0
- package/packages/piagent-webui/package.json +1 -1
|
@@ -6,7 +6,7 @@ import fs from 'node:fs';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
8
|
import { MemoryOutbox, memoryScope, scopeID } from './memory-outbox.mjs';
|
|
9
|
-
import { CheckLessons } from './memory-lessons.mjs';
|
|
9
|
+
import { CheckLessons } from './memory-lessons.mjs'; import { SkillFeedback } from './skill-feedback.mjs'; import { RoundServes, FEEDBACK_PROTOCOL } from './memory-serves.mjs';
|
|
10
10
|
import { CLIENT_AGENT } from './client-agent.mjs'; import { knownCapabilities, refreshCapabilities } from './studio-capabilities.mjs';
|
|
11
11
|
import { redactedText } from '../runtime/managed-failure.mjs';
|
|
12
12
|
import { git, repoKey, repositoryAt, findRepositories, topDirs, eventFor, commitChanges } from './memory-repos.mjs';
|
|
@@ -43,7 +43,7 @@ export function turnRecord(messages, changedFiles = [], lessons = []) {
|
|
|
43
43
|
};
|
|
44
44
|
const parts = [];
|
|
45
45
|
// Preserve evidence and the final answer before budgeting member input.
|
|
46
|
-
const lessonText = lessons.slice(0, 3).map(l => `${l.line} Evidence: ${l.failed_call} → ${l.passed_call}; fingerprint ${l.fingerprint}; ${l.at}.`).join('\n');
|
|
46
|
+
const lessonText = lessons.slice(0, 3).map(l => `${l.line} Evidence: ${l.failed_call} ${l.kind === 'tool_error' ? 'and' : '→'} ${l.passed_call}; fingerprint ${l.fingerprint}; ${l.at}.`).join('\n');
|
|
47
47
|
const memberBudget = MAX_TURN - 13000 - Buffer.byteLength(lessonText);
|
|
48
48
|
const member = safeClip(asked.map(a => `Member: ${a}`).join('\n\n'), memberBudget);
|
|
49
49
|
if (member) parts.push(member);
|
|
@@ -68,7 +68,7 @@ function bounded(promise, signal, late = () => {}) {
|
|
|
68
68
|
|
|
69
69
|
export class TeamMemory {
|
|
70
70
|
constructor({ origin, cwd, stateDir, conversationId, scope = () => null, lease = null, renew = null, fetchImpl = fetch, now = () => Date.now(), changed = () => [] }) {
|
|
71
|
-
this.renew = renew; this.scope = scope; this.lease = lease; this.protocol = 0; this.background = null; this.stats = {};
|
|
71
|
+
this.renew = renew; this.scope = scope; this.lease = lease; this.protocol = 0; this.background = null; this.stats = {}; this.serves = new RoundServes();
|
|
72
72
|
Object.assign(this, { origin, cwd, stateDir, conversationId, fetchImpl, now, changed, repo: undefined, repos: undefined, folder: null, heads: {}, offUntil: 0, retain: true, briefed: false });
|
|
73
73
|
this.syncScope();
|
|
74
74
|
this.knownPath = stateDir ? path.join(stateDir, 'memory-known.json') : null;
|
|
@@ -215,7 +215,7 @@ export class TeamMemory {
|
|
|
215
215
|
this.retain = answered.every(([, b]) => b.retain_sessions !== false);
|
|
216
216
|
this.count('brief_successes'); if (this.lastBrief === 'ok') this.count('brief_nonempty');
|
|
217
217
|
const heads = {};
|
|
218
|
-
for (const [r, b] of answered) heads[r.key] = b.git_head ?? null;
|
|
218
|
+
for (const [r, b] of answered) { heads[r.key] = b.git_head ?? null; this.serves.add(r.key, b.serve_id); }
|
|
219
219
|
this.startBackground(grant, heads);
|
|
220
220
|
const several = repos.length > 1, pages = new Set(), parts = [];
|
|
221
221
|
let facts = 0;
|
|
@@ -309,11 +309,12 @@ export class TeamMemory {
|
|
|
309
309
|
if (own.length) item.files = own.slice(0, 12).map(f => String(f).slice(0, 200));
|
|
310
310
|
if (checks) item.checks = checks;
|
|
311
311
|
}
|
|
312
|
+
if (this.protocol >= FEEDBACK_PROTOCOL && this.serves.of(r.key).length) item.serves = this.serves.of(r.key);
|
|
312
313
|
if (!box.put(item)) { this.count('queue_rejected'); continue; }
|
|
313
314
|
this.count('queued'); queued = true;
|
|
314
315
|
}
|
|
315
|
-
// A rebind cannot send the old round using the new key.
|
|
316
|
-
if (!queued || scopeID(source) !== scopeID(this.scope())) return;
|
|
316
|
+
// A serve belongs to the round that used it. A rebind cannot send the old round using the new key.
|
|
317
|
+
this.serves.clear(); if (!queued || scopeID(source) !== scopeID(this.scope())) return;
|
|
317
318
|
await this.#drain(grant, deadline);
|
|
318
319
|
}
|
|
319
320
|
|
|
@@ -331,7 +332,7 @@ export class TeamMemory {
|
|
|
331
332
|
try {
|
|
332
333
|
if (claim.record.attempts) this.count('write_retries');
|
|
333
334
|
// A round queued for protocol 3 meets a Studio that went back to 2: send the fields 2 knows.
|
|
334
|
-
const item = this.protocol >=
|
|
335
|
+
const { serves, ...v3 } = claim.record.item, item = this.protocol >= FEEDBACK_PROTOCOL ? claim.record.item : this.protocol >= PROTOCOL ? v3 : (({ context, files, checks, ...v2 }) => v2)(v3);
|
|
335
336
|
const ack = await this.#post(grant, 'retain', item, WRITE_MS, this.renew, signal);
|
|
336
337
|
if (!ack || ack.status === 'retain_disabled') { this.retain = false; outcome = 'discard'; }
|
|
337
338
|
else if (ack.event_id === claim.record.item.event_id && ack.operation_id && ['pending','processing','completed','failed','cancelled','unknown'].includes(ack.status)) {
|
|
@@ -369,7 +370,7 @@ export class TeamMemory {
|
|
|
369
370
|
if (repository && !picked && this.repos.length > 1) return this.#none(repository);
|
|
370
371
|
this.count(renew?.role && renew.role !== 'main' ? 'helper_search_calls' : 'search_calls');
|
|
371
372
|
return this.#each(picked ? [picked] : this.#about(query), async r => {
|
|
372
|
-
try {
|
|
373
|
+
try { const answer = await this.#post(grant, 'search', await this.#body(r, { query }), TOOL_MS, renew ?? this.renew); this.serves.add(r.key, answer?.serve_id); return answer?.text ?? 'Team memory is off for this team.'; }
|
|
373
374
|
catch { return 'Team memory did not answer; go on without it.'; }
|
|
374
375
|
});
|
|
375
376
|
}
|
|
@@ -447,7 +448,7 @@ export function attachMemory(self, manager, agentDir, verify) {
|
|
|
447
448
|
return self.memory;
|
|
448
449
|
}
|
|
449
450
|
export function trackMemoryLessons(self) {
|
|
450
|
-
self.session.subscribe(event => { try { self.memoryTurn?.checks?.event(event); } catch { /* optional evidence */ } });
|
|
451
|
+
self.session.subscribe(event => { try { self.memoryTurn?.checks?.event(event); self.memoryTurn?.skills?.event(event); } catch { /* optional evidence */ } });
|
|
451
452
|
}
|
|
452
453
|
// Whether this conversation has team memory now: Studio answered memory for
|
|
453
454
|
// this key recently and has not switched it off since (the Terminal's header).
|
|
@@ -464,7 +465,7 @@ export const helperMemoryTools = (managed, grant, renew) => managed.memory?.stat
|
|
|
464
465
|
export async function openRound(self, manager, text) {
|
|
465
466
|
if (!self.memory) return;
|
|
466
467
|
self.memory.stats = {};
|
|
467
|
-
const turn = self.memoryTurn = { id: randomUUID(), at: new Date().toISOString(), scope: memoryScope(self.origin, self.manifest), request: text, users: [text], checks: new CheckLessons(self.checks?.commands ?? []) };
|
|
468
|
+
const turn = self.memoryTurn = { id: randomUUID(), at: new Date().toISOString(), scope: memoryScope(self.origin, self.manifest), request: text, users: [text], checks: new CheckLessons(self.checks?.commands ?? []), skills: SkillFeedback.forRound(self, text) };
|
|
468
469
|
turn.entry = manager.appendCustomEntry('agent-watch-memory-turn', { id: turn.id, at: turn.at, scope: turn.scope });
|
|
469
470
|
try {
|
|
470
471
|
// Preserve the first brief in history; later refreshes are injected by the
|
|
@@ -479,6 +480,7 @@ export async function openRound(self, manager, text) {
|
|
|
479
480
|
} catch { /* memory is optional */ }
|
|
480
481
|
}
|
|
481
482
|
export async function closeRound(self) {
|
|
483
|
+
try { const t = self.memoryTurn; t?.skills?.close({ checks: t.checks?.last, lessons: t.checks?.lessons ?? [] }); } catch { /* optional evidence */ }
|
|
482
484
|
if (!self.grant || self.preflight || !self.memory || !self.memoryTurn) return;
|
|
483
485
|
const turn = self.memoryTurn, closing = AbortSignal.timeout(CLOSE_MS), started = Date.now();
|
|
484
486
|
let outcome = 'unknown';
|
|
@@ -257,7 +257,7 @@ function capacityStatus(owner) {
|
|
|
257
257
|
// run) fails at once, as before. Pi decides by the words of an error, which
|
|
258
258
|
// for a company failure would match "rate limited" or "timeout" in sentences
|
|
259
259
|
// that must not be retried; here it decides by Studio's code only.
|
|
260
|
-
export const TURN_RETRY_CODES = new Set(['upstream_interrupted', 'upstream_incomplete', 'upstream_timeout', 'upstream_unavailable', 'output_limit_exceeded',
|
|
260
|
+
export const TURN_RETRY_CODES = new Set(['upstream_interrupted', 'upstream_incomplete', 'upstream_timeout', 'upstream_unavailable', 'upstream_server_error', 'output_limit_exceeded',
|
|
261
261
|
'connector_unavailable', 'connector_execution_failed', 'inference_unavailable', 'studio-unreachable']);
|
|
262
262
|
export const TURN_RETRY = { maxRetries: 2, baseDelayMs: 2_000 };
|
|
263
263
|
|
|
@@ -278,7 +278,13 @@ export function enableCompanyTurnRetry(session, settings, link = null) {
|
|
|
278
278
|
settings.applyOverrides({ retry: { enabled: true, ...TURN_RETRY } });
|
|
279
279
|
const prepare = session._prepareRetry;
|
|
280
280
|
if (link && typeof prepare === 'function') session._prepareRetry = async function (message) {
|
|
281
|
-
|
|
281
|
+
// Each ask again goes to the member's local log (studio-link.jsonl), so a
|
|
282
|
+
// task that stopped or went on can be read back later.
|
|
283
|
+
if (!studio(message)) {
|
|
284
|
+
const asked = await prepare.call(this, message);
|
|
285
|
+
if (asked) link.log({ kind: 'turn_retry', code: failureCode(message.errorMessage), attempt: this._retryAttempt });
|
|
286
|
+
return asked;
|
|
287
|
+
}
|
|
282
288
|
// Studio answers here and was not away lately (Agent Watch or Studio's
|
|
283
289
|
// connector failed on a running Studio): Pi's own asks again, as before.
|
|
284
290
|
if (!(await link.needed(failureCode(message.errorMessage)).catch(() => false))) return companyTurnRetryable(message) ? prepare.call(this, message) : false;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Skill feedback (after Cognee's proposal-first skill improvement): which
|
|
2
|
+
// skills a round used, and whether the round ended badly (its last check
|
|
3
|
+
// failed, or a command failed the same way twice). A skill that keeps ending
|
|
4
|
+
// badly gets a proposal file to review under .pi/piagent-state/skill-proposals;
|
|
5
|
+
// Piagent never edits a skill itself and makes no model call for this.
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
const MIN_FAILURES = 3, MIN_SHARE = 0.5, KEEP = 10, REPROPOSE_MS = 7 * 86400_000;
|
|
10
|
+
const safeName = name => String(name).replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 80);
|
|
11
|
+
|
|
12
|
+
export class SkillFeedback {
|
|
13
|
+
// A managed round's tracker, when the conversation has skills.
|
|
14
|
+
static forRound(self, text) {
|
|
15
|
+
if (!self.resources?.skills?.length) return null;
|
|
16
|
+
const feedback = new SkillFeedback({ cwd: self.cwd, skills: self.resources.skills });
|
|
17
|
+
feedback.request(text);
|
|
18
|
+
return feedback;
|
|
19
|
+
}
|
|
20
|
+
constructor({ cwd, skills = [], now = Date.now }) {
|
|
21
|
+
this.cwd = cwd; this.now = now; this.used = new Set();
|
|
22
|
+
this.skills = skills.filter(s => s?.name && s?.filePath).map(s => ({ name: s.name, file: path.resolve(s.filePath) }));
|
|
23
|
+
}
|
|
24
|
+
// A member's /skill:name arrives expanded as a <skill name="..."> block.
|
|
25
|
+
request(text) {
|
|
26
|
+
for (const m of String(text ?? '').matchAll(/<skill name="([^"]{1,80})"/g)) if (this.skills.some(s => s.name === m[1])) this.used.add(m[1]);
|
|
27
|
+
}
|
|
28
|
+
// The model reading a skill's SKILL.md is the skill being used.
|
|
29
|
+
event(event) {
|
|
30
|
+
if (event?.type !== 'tool_execution_start' || event.toolName !== 'read' || typeof event.args?.path !== 'string') return;
|
|
31
|
+
const file = path.resolve(this.cwd, event.args.path);
|
|
32
|
+
for (const s of this.skills) if (s.file === file) this.used.add(s.name);
|
|
33
|
+
}
|
|
34
|
+
// The round's end: counts for every skill it used; returns the proposals written.
|
|
35
|
+
close({ checks = 'none', lessons = [] } = {}) {
|
|
36
|
+
if (!this.used.size || !this.cwd) return [];
|
|
37
|
+
const errors = lessons.filter(l => l.kind === 'tool_error');
|
|
38
|
+
const bad = checks === 'failed' || errors.length > 0;
|
|
39
|
+
const written = [];
|
|
40
|
+
let dir;
|
|
41
|
+
// Inside the project's own state folder only: never through a link.
|
|
42
|
+
try {
|
|
43
|
+
const state = path.join(fs.realpathSync(this.cwd), '.pi', 'piagent-state');
|
|
44
|
+
dir = path.join(state, 'skill-proposals');
|
|
45
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
46
|
+
if (fs.realpathSync(dir) !== dir || fs.lstatSync(dir).isSymbolicLink()) return [];
|
|
47
|
+
} catch { return []; }
|
|
48
|
+
for (const name of this.used) {
|
|
49
|
+
const file = path.join(dir, `${safeName(name)}.json`);
|
|
50
|
+
let state = { name, uses: 0, failures: [], proposed_at: null };
|
|
51
|
+
try { state = { ...state, ...JSON.parse(fs.readFileSync(file, 'utf8')) }; } catch { /* first use */ }
|
|
52
|
+
state.uses++;
|
|
53
|
+
if (bad) {
|
|
54
|
+
const detail = checks === 'failed' ? 'the round\'s last check failed' : errors[0].line;
|
|
55
|
+
state.failures = [...state.failures, { at: new Date(this.now()).toISOString(), detail: String(detail).slice(0, 400) }].slice(-KEEP);
|
|
56
|
+
}
|
|
57
|
+
const recent = state.failures.length, share = recent / Math.min(state.uses, KEEP);
|
|
58
|
+
const due = recent >= MIN_FAILURES && share >= MIN_SHARE && (!state.proposed_at || this.now() - Date.parse(state.proposed_at) > REPROPOSE_MS);
|
|
59
|
+
if (due) {
|
|
60
|
+
state.proposed_at = new Date(this.now()).toISOString();
|
|
61
|
+
const skill = this.skills.find(s => s.name === name);
|
|
62
|
+
const proposal = path.join(dir, `${safeName(name)}.md`);
|
|
63
|
+
fs.writeFileSync(proposal, [
|
|
64
|
+
`# Review the skill "${name}"`, '',
|
|
65
|
+
`Proposed ${state.proposed_at} by Piagent. Nothing was changed: read the evidence, then edit the skill yourself or delete this file.`, '',
|
|
66
|
+
`Skill file: ${skill ? path.relative(this.cwd, skill.file) || skill.file : 'unknown'}`, '',
|
|
67
|
+
`${recent} of the last ${Math.min(state.uses, KEEP)} rounds that used it ended badly:`, '',
|
|
68
|
+
...state.failures.map(f => `- ${f.at}: ${f.detail}`), '',
|
|
69
|
+
'Look for: a step the skill tells the agent to run that no longer works here (a command, flag, path or check),',
|
|
70
|
+
'instructions that contradict the repository\'s current conventions, or a missing prerequisite.', ''].join('\n'), { mode: 0o600 });
|
|
71
|
+
written.push(proposal);
|
|
72
|
+
state.failures = [];
|
|
73
|
+
}
|
|
74
|
+
fs.writeFileSync(file, JSON.stringify(state, null, 1), { mode: 0o600 });
|
|
75
|
+
}
|
|
76
|
+
this.used.clear();
|
|
77
|
+
return written;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -151,6 +151,16 @@ export class StudioLink {
|
|
|
151
151
|
}
|
|
152
152
|
justBack() { return !!this.recoveredAt && this.now() - this.recoveredAt < (this.brokerGraceMs ?? BROKER_GRACE_MS); }
|
|
153
153
|
|
|
154
|
+
// A pause Stop can end (a round asked again after a provider's passing failure).
|
|
155
|
+
async hold(ms, signal) {
|
|
156
|
+
const controller = new AbortController(), stop = () => controller.abort(signal?.reason);
|
|
157
|
+
if (signal?.aborted) throw signal.reason ?? Error('aborted');
|
|
158
|
+
signal?.addEventListener('abort', stop, { once: true });
|
|
159
|
+
this.waiting.add(controller);
|
|
160
|
+
try { await this.pause(ms, controller.signal); }
|
|
161
|
+
finally { signal?.removeEventListener('abort', stop); this.waiting.delete(controller); if (!this.waiting.size) this.status(undefined); }
|
|
162
|
+
}
|
|
163
|
+
|
|
154
164
|
// Stop ends every wait at once.
|
|
155
165
|
cancel() { for (const controller of this.waiting) controller.abort(Error('managed-request-aborted')); }
|
|
156
166
|
|
|
@@ -211,6 +221,14 @@ export class StudioLink {
|
|
|
211
221
|
}
|
|
212
222
|
}
|
|
213
223
|
|
|
224
|
+
// What the main agent reads when its work goes on after the model provider
|
|
225
|
+
// failed for a passing reason in the middle of the round.
|
|
226
|
+
export function providerResumeMessage(code, attempt, max) {
|
|
227
|
+
return { customType: 'agent-watch-process', display: true,
|
|
228
|
+
content: `Harness: your last step was cut off because the model provider failed while answering [${code}]; that is a passing failure on its side (attempt ${attempt} of ${max} to go on). 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.`,
|
|
229
|
+
details: { phase: 'resume', cause: 'provider', code, attempt, max } };
|
|
230
|
+
}
|
|
231
|
+
|
|
214
232
|
// What the main agent reads when its work goes on after Studio was back.
|
|
215
233
|
export function resumeMessage(wait, code) {
|
|
216
234
|
const why = { studio_update: `Studio was updated${wait.version_before && wait.version_after ? ` (${wait.version_before} → ${wait.version_after})` : ''}`,
|
|
@@ -52,6 +52,10 @@ const CODES = {
|
|
|
52
52
|
managed_service_unavailable: ['service', 'Studio could not start the run'],
|
|
53
53
|
upstream_interrupted: ['service', 'the model response was interrupted'],
|
|
54
54
|
upstream_incomplete: ['service', 'the model returned no response'],
|
|
55
|
+
// The provider's own error event in the middle of an answer (OpenAI
|
|
56
|
+
// server_error / api_error, Anthropic api_error / overloaded_error): a
|
|
57
|
+
// passing failure on its side, asked again.
|
|
58
|
+
upstream_server_error: ['service', 'the model provider failed while answering (a passing error on its side)'],
|
|
55
59
|
// Studio ended the stream: the model wrote past the request's output limit
|
|
56
60
|
// (a degenerate answer), or the request was ended for the member.
|
|
57
61
|
output_limit_exceeded: ['service', 'the model kept writing past its output limit and was stopped'],
|
|
@@ -104,6 +108,7 @@ const CODES = {
|
|
|
104
108
|
};
|
|
105
109
|
const ROLES = { main: 'main agent', scout: 'scout subagent', research: 'research subagent', verify: 'verify subagent', review: 'review subagent' };
|
|
106
110
|
const REQUEST = /\b([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b/;
|
|
111
|
+
const PROVIDER_SERVER_ERROR = /^(?:Error Code )?(?:server_error|api_error|internal_error|internal_server_error|overloaded_error|undefined):|An error occurred while processing your request|"type"\s*:\s*"(?:api_error|overloaded_error)"|\bOverloaded\b/i;
|
|
107
112
|
const NETWORK = /fetch failed|econnrefused|econnreset|enotfound|ehostunreach|etimedout|socket hang up|network|connection error|terminated|timed? ?out|aborted due to timeout/i;
|
|
108
113
|
|
|
109
114
|
export function failureKind(code) { return CODES[code]?.[0] ?? 'failed'; }
|
|
@@ -151,6 +156,9 @@ export function failureCode(raw) {
|
|
|
151
156
|
const local = /(managed-broker:[A-Za-z_]{1,80}|managed-[a-z-]{1,80}|web-search-empty)/.exec(text)?.[1];
|
|
152
157
|
if (known(local)) return local;
|
|
153
158
|
if (!studio && /\bAPI error \((?:50[234]|52\d|530)\)|^(?:50[234]|52\d|530)\b/.test(text)) return 'studio-gateway-unavailable';
|
|
159
|
+
// How Pi's providers word the provider's error event inside a streamed
|
|
160
|
+
// answer (seen 2026-10-21: gpt-6-sol sent api_error mid-answer and the task stopped).
|
|
161
|
+
if (PROVIDER_SERVER_ERROR.test(text)) return 'upstream_server_error';
|
|
154
162
|
if (studio || /\bAPI error \(\d{3}\)|^\d{3} \{/.test(text)) return 'studio-request-failed';
|
|
155
163
|
// The provider declined the request itself (its usage policy), not Studio:
|
|
156
164
|
// the same request is declined again, so it is said in words.
|