@pi-unipi/unipi 2.5.0 → 2.6.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 +24 -0
- package/README.md +2 -0
- package/docs/prefix-cache-architecture.md +89 -0
- package/package.json +22 -21
- package/packages/ask-user/package.json +2 -2
- package/packages/autocomplete/package.json +1 -1
- package/packages/autocomplete/src/constants.ts +2 -0
- package/packages/btw/package.json +2 -2
- package/packages/cocoindex/README.md +2 -1
- package/packages/cocoindex/index.ts +6 -13
- package/packages/cocoindex/package.json +4 -3
- package/packages/cocoindex/tools.ts +45 -11
- package/packages/compactor/README.md +4 -2
- package/packages/compactor/package.json +3 -3
- package/packages/compactor/src/session/snapshot.ts +3 -2
- package/packages/compactor/src/tools/register.ts +6 -2
- package/packages/compactor/src/tools/vcc-recall.ts +18 -3
- package/packages/core/bounded-output.ts +106 -0
- package/packages/core/constants.ts +2 -0
- package/packages/core/index.ts +1 -0
- package/packages/core/package.json +1 -1
- package/packages/footer/package.json +2 -2
- package/packages/image/package.json +2 -2
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/kanboard/package.json +2 -2
- package/packages/mcp/README.md +4 -0
- package/packages/mcp/package.json +2 -2
- package/packages/mcp/src/bridge/translator.ts +19 -12
- package/packages/memory/index.ts +56 -34
- package/packages/memory/package.json +3 -3
- package/packages/milestone/hooks.ts +1 -1
- package/packages/milestone/package.json +2 -2
- package/packages/notify/package.json +2 -2
- package/packages/ralph/index.ts +12 -18
- package/packages/ralph/package.json +6 -3
- package/packages/ralph/reminder.ts +40 -0
- package/packages/ralph/tools.ts +5 -1
- package/packages/subagents/README.md +2 -0
- package/packages/subagents/package.json +1 -1
- package/packages/subagents/src/agent-manager.ts +5 -1
- package/packages/subagents/src/agent-runner.ts +2 -2
- package/packages/subagents/src/core-compat.ts +73 -0
- package/packages/subagents/src/custom-agents.ts +10 -2
- package/packages/subagents/src/index.ts +14 -3
- package/packages/subagents/src/types.ts +2 -0
- package/packages/unipi/bundled.js +786 -312
- package/packages/updater/package.json +2 -2
- package/packages/utility/README.md +9 -0
- package/packages/utility/package.json +2 -2
- package/packages/utility/src/index.ts +48 -0
- package/packages/utility/src/lifecycle/cleanup.ts +29 -0
- package/packages/utility/src/prefix-cache.ts +263 -0
- package/packages/utility/src/types.ts +1 -1
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/index.ts +2 -2
- package/packages/workflow/package.json +2 -2
|
@@ -13,6 +13,10 @@ import { resolveModel, type ModelRegistry } from "./model-resolver.js";
|
|
|
13
13
|
import type { AgentRecord, AgentConfig, AgentType, ThinkingLevel, SubagentsConfig } from "./types.js";
|
|
14
14
|
import { BUILTIN_CONFIGS } from "./types.js";
|
|
15
15
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
16
|
+
|
|
17
|
+
function compareCodeUnits(a: string, b: string): number {
|
|
18
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
19
|
+
}
|
|
16
20
|
import { FileLock } from "./file-lock.js";
|
|
17
21
|
|
|
18
22
|
export type OnAgentComplete = (record: AgentRecord) => void;
|
|
@@ -88,7 +92,7 @@ export class AgentManager {
|
|
|
88
92
|
...Object.keys(BUILTIN_CONFIGS).filter((type) => type !== "name-gen"),
|
|
89
93
|
...this.customAgents.keys(),
|
|
90
94
|
...Object.keys(this.typeSettings),
|
|
91
|
-
])];
|
|
95
|
+
])].sort(compareCodeUnits);
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
/** A type is enabled only when both JSON config and agent frontmatter allow it. */
|
|
@@ -19,8 +19,8 @@ import {
|
|
|
19
19
|
} from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { BUILTIN_CONFIGS, type AgentConfig, type AgentType, type ThinkingLevel } from "./types.js";
|
|
21
21
|
|
|
22
|
-
/** Tools excluded from subagents to prevent nesting. */
|
|
23
|
-
const EXCLUDED_TOOL_NAMES = ["Agent", "get_result"];
|
|
22
|
+
/** Tools excluded from subagents to prevent nesting. Legacy names remain for older hosts. */
|
|
23
|
+
const EXCLUDED_TOOL_NAMES = ["spawn_helper", "get_helper_result", "Agent", "get_result"];
|
|
24
24
|
|
|
25
25
|
/** All known built-in tool names. */
|
|
26
26
|
const BUILTIN_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { chmodSync, lstatSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
2
6
|
|
|
3
7
|
export const MODULES = { SUBAGENTS: "subagents" } as const;
|
|
4
8
|
|
|
@@ -24,6 +28,75 @@ export function emitEvent(
|
|
|
24
28
|
}
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
export interface BoundedHelperOutput {
|
|
32
|
+
text: string;
|
|
33
|
+
truncated: boolean;
|
|
34
|
+
originalBytes: number;
|
|
35
|
+
artifactPath?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Standalone-package twin of core's bounded-output policy. */
|
|
39
|
+
const MAX_RAW_HELPER_ARTIFACT_BYTES = 16 * 1024 * 1024;
|
|
40
|
+
|
|
41
|
+
export function boundHelperOutput(
|
|
42
|
+
text: string,
|
|
43
|
+
maxBytes = 64 * 1024,
|
|
44
|
+
existingArtifactPath?: string,
|
|
45
|
+
): BoundedHelperOutput {
|
|
46
|
+
const originalBytes = Buffer.byteLength(text, "utf8");
|
|
47
|
+
if (originalBytes <= maxBytes) return { text, truncated: false, originalBytes };
|
|
48
|
+
|
|
49
|
+
let artifactPath = existingArtifactPath;
|
|
50
|
+
let artifactWarning: string | undefined;
|
|
51
|
+
if (!artifactPath && originalBytes <= MAX_RAW_HELPER_ARTIFACT_BYTES) {
|
|
52
|
+
try {
|
|
53
|
+
const dir = join(homedir(), ".unipi", "tool-results");
|
|
54
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
55
|
+
let stat = lstatSync(dir);
|
|
56
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
57
|
+
throw new Error(`Refusing unsafe tool-result directory: ${dir}`);
|
|
58
|
+
}
|
|
59
|
+
if ((stat.mode & 0o077) !== 0) {
|
|
60
|
+
chmodSync(dir, 0o700);
|
|
61
|
+
stat = lstatSync(dir);
|
|
62
|
+
if ((stat.mode & 0o077) !== 0) throw new Error(`Refusing non-private tool-result directory: ${dir}`);
|
|
63
|
+
}
|
|
64
|
+
artifactPath = join(dir, `helper-${randomUUID()}.txt`);
|
|
65
|
+
writeFileSync(artifactPath, text, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
66
|
+
} catch (error) {
|
|
67
|
+
artifactWarning = `Full-output artifact unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
68
|
+
artifactPath = undefined;
|
|
69
|
+
}
|
|
70
|
+
} else if (!artifactPath) {
|
|
71
|
+
artifactWarning = `Full output exceeded the ${MAX_RAW_HELPER_ARTIFACT_BYTES}-byte local artifact safety cap and was not retained.`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const marker = [
|
|
75
|
+
"",
|
|
76
|
+
"--- output bounded by UniPi ---",
|
|
77
|
+
artifactPath ? `Full output: ${artifactPath}` : artifactWarning!,
|
|
78
|
+
`Original size: ${originalBytes} bytes; model-visible ceiling: ${maxBytes} bytes.`,
|
|
79
|
+
...(artifactPath ? ["Use the read tool with offset/limit to inspect only the needed region."] : []),
|
|
80
|
+
].join("\n");
|
|
81
|
+
const markerBytes = Buffer.byteLength(marker, "utf8");
|
|
82
|
+
const contentBudget = Math.max(1, maxBytes - markerBytes - 80);
|
|
83
|
+
const bytes = Buffer.from(text, "utf8");
|
|
84
|
+
const headBytes = Math.ceil(contentBudget * 0.75);
|
|
85
|
+
const tailBytes = Math.max(0, contentBudget - headBytes);
|
|
86
|
+
const head = bytes.subarray(0, headBytes).toString("utf8").replace(/\uFFFD+$/u, "");
|
|
87
|
+
const tail = bytes.subarray(originalBytes - tailBytes).toString("utf8").replace(/^\uFFFD+/u, "");
|
|
88
|
+
let bounded = `${head}\n… ${Math.max(0, originalBytes - contentBudget)} bytes omitted …\n${tail}${marker}`;
|
|
89
|
+
if (Buffer.byteLength(bounded, "utf8") > maxBytes) {
|
|
90
|
+
bounded = Buffer.from(bounded, "utf8").subarray(0, maxBytes).toString("utf8").replace(/\uFFFD+$/u, "");
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
text: bounded,
|
|
94
|
+
truncated: true,
|
|
95
|
+
originalBytes,
|
|
96
|
+
artifactPath,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
27
100
|
export async function withHerdrBlocked<T>(
|
|
28
101
|
pi: Pick<ExtensionAPI, "events">,
|
|
29
102
|
label: string,
|
|
@@ -12,6 +12,10 @@ import { homedir } from "node:os";
|
|
|
12
12
|
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import type { AgentConfig } from "./types.js";
|
|
14
14
|
|
|
15
|
+
function compareCodeUnits(a: string, b: string): number {
|
|
16
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
15
19
|
/** Backup a corrupted file by renaming to .bak */
|
|
16
20
|
function backupCorrupted(filePath: string): void {
|
|
17
21
|
const backupPath = filePath + ".bak";
|
|
@@ -93,7 +97,9 @@ export function loadCustomAgents(cwd: string): Map<string, AgentConfig> {
|
|
|
93
97
|
// Load global agents first
|
|
94
98
|
const globalDir = getGlobalAgentsDir();
|
|
95
99
|
if (existsSync(globalDir)) {
|
|
96
|
-
const files = readdirSync(globalDir)
|
|
100
|
+
const files = readdirSync(globalDir)
|
|
101
|
+
.filter((f) => f.endsWith(".md"))
|
|
102
|
+
.sort(compareCodeUnits);
|
|
97
103
|
for (const file of files) {
|
|
98
104
|
const agent = loadAgentFromFile(join(globalDir, file), "global");
|
|
99
105
|
if (agent) {
|
|
@@ -105,7 +111,9 @@ export function loadCustomAgents(cwd: string): Map<string, AgentConfig> {
|
|
|
105
111
|
// Load project agents (overrides global)
|
|
106
112
|
const projectDir = getProjectAgentsDir(cwd);
|
|
107
113
|
if (existsSync(projectDir)) {
|
|
108
|
-
const files = readdirSync(projectDir)
|
|
114
|
+
const files = readdirSync(projectDir)
|
|
115
|
+
.filter((f) => f.endsWith(".md"))
|
|
116
|
+
.sort(compareCodeUnits);
|
|
109
117
|
for (const file of files) {
|
|
110
118
|
const agent = loadAgentFromFile(join(projectDir, file), "project");
|
|
111
119
|
if (agent) {
|
|
@@ -12,7 +12,7 @@ import { Type } from "typebox";
|
|
|
12
12
|
import { existsSync, readdirSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
|
-
import { emitEvent, MODULES, UNIPI_EVENTS, withHerdrBlocked, type UnipiBadgeGenerateRequestEvent } from "./core-compat.js";
|
|
15
|
+
import { boundHelperOutput, emitEvent, MODULES, UNIPI_EVENTS, withHerdrBlocked, type UnipiBadgeGenerateRequestEvent } from "./core-compat.js";
|
|
16
16
|
import { AgentManager } from "./agent-manager.js";
|
|
17
17
|
import { initConfig } from "./config.js";
|
|
18
18
|
import { type AgentActivity, type NotificationDetails, BUILTIN_TYPES } from "./types.js";
|
|
@@ -756,9 +756,11 @@ Guidelines:
|
|
|
756
756
|
});
|
|
757
757
|
}
|
|
758
758
|
|
|
759
|
+
const output = boundHelperOutput(record.result?.trim() || "No output.");
|
|
760
|
+
record.resultArtifactPath = output.artifactPath;
|
|
759
761
|
return textResult(
|
|
760
762
|
`Agent completed in ${(durationMs / 1000).toFixed(1)}s (${record.toolUses} tool uses${tokenText ? `, ${tokenText} tokens` : ""}).\n\n` +
|
|
761
|
-
|
|
763
|
+
output.text,
|
|
762
764
|
{
|
|
763
765
|
status: "completed",
|
|
764
766
|
toolUses: record.toolUses,
|
|
@@ -766,6 +768,9 @@ Guidelines:
|
|
|
766
768
|
durationMs,
|
|
767
769
|
turnCount: fgState.turnCount,
|
|
768
770
|
maxTurns: fgState.maxTurns,
|
|
771
|
+
truncated: output.truncated,
|
|
772
|
+
originalBytes: output.originalBytes,
|
|
773
|
+
artifactPath: output.artifactPath,
|
|
769
774
|
},
|
|
770
775
|
);
|
|
771
776
|
},
|
|
@@ -851,7 +856,13 @@ Guidelines:
|
|
|
851
856
|
} else if (record.status === "error") {
|
|
852
857
|
output += `Error: ${record.error}`;
|
|
853
858
|
} else {
|
|
854
|
-
|
|
859
|
+
const bounded = boundHelperOutput(
|
|
860
|
+
record.result?.trim() || "No output.",
|
|
861
|
+
64 * 1024,
|
|
862
|
+
record.resultArtifactPath,
|
|
863
|
+
);
|
|
864
|
+
record.resultArtifactPath = bounded.artifactPath;
|
|
865
|
+
output += bounded.text;
|
|
855
866
|
}
|
|
856
867
|
|
|
857
868
|
if (record.status !== "running" && record.status !== "queued") {
|
|
@@ -90,6 +90,8 @@ export interface AgentRecord {
|
|
|
90
90
|
description: string;
|
|
91
91
|
status: "queued" | "running" | "completed" | "aborted" | "stopped" | "error";
|
|
92
92
|
result?: string;
|
|
93
|
+
/** Private artifact holding the complete result when model-visible output is bounded. */
|
|
94
|
+
resultArtifactPath?: string;
|
|
93
95
|
error?: string;
|
|
94
96
|
toolUses: number;
|
|
95
97
|
startedAt: number;
|