@piagent/platform 1.35.1 → 1.36.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/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/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/session.mjs +3 -3
- package/packages/piagent-core/managed/skill-feedback.mjs +79 -0
- package/packages/piagent-core/managed/studio-link.mjs +34 -4
- package/packages/piagent-core/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -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(`
|
|
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
|
+
}
|