@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +2 -0
  3. package/docs/prefix-cache-architecture.md +89 -0
  4. package/package.json +22 -21
  5. package/packages/ask-user/package.json +2 -2
  6. package/packages/autocomplete/package.json +1 -1
  7. package/packages/autocomplete/src/constants.ts +2 -0
  8. package/packages/btw/package.json +2 -2
  9. package/packages/cocoindex/README.md +2 -1
  10. package/packages/cocoindex/index.ts +6 -13
  11. package/packages/cocoindex/package.json +4 -3
  12. package/packages/cocoindex/tools.ts +45 -11
  13. package/packages/compactor/README.md +4 -2
  14. package/packages/compactor/package.json +3 -3
  15. package/packages/compactor/src/session/snapshot.ts +3 -2
  16. package/packages/compactor/src/tools/register.ts +6 -2
  17. package/packages/compactor/src/tools/vcc-recall.ts +18 -3
  18. package/packages/core/bounded-output.ts +106 -0
  19. package/packages/core/constants.ts +2 -0
  20. package/packages/core/index.ts +1 -0
  21. package/packages/core/package.json +1 -1
  22. package/packages/footer/package.json +2 -2
  23. package/packages/image/package.json +2 -2
  24. package/packages/info-screen/package.json +2 -2
  25. package/packages/input-shortcuts/package.json +2 -2
  26. package/packages/kanboard/package.json +2 -2
  27. package/packages/mcp/README.md +4 -0
  28. package/packages/mcp/package.json +2 -2
  29. package/packages/mcp/src/bridge/translator.ts +19 -12
  30. package/packages/memory/index.ts +56 -34
  31. package/packages/memory/package.json +3 -3
  32. package/packages/milestone/hooks.ts +1 -1
  33. package/packages/milestone/package.json +2 -2
  34. package/packages/notify/package.json +2 -2
  35. package/packages/ralph/index.ts +12 -18
  36. package/packages/ralph/package.json +6 -3
  37. package/packages/ralph/reminder.ts +40 -0
  38. package/packages/ralph/tools.ts +5 -1
  39. package/packages/subagents/README.md +2 -0
  40. package/packages/subagents/package.json +1 -1
  41. package/packages/subagents/src/agent-manager.ts +5 -1
  42. package/packages/subagents/src/agent-runner.ts +2 -2
  43. package/packages/subagents/src/core-compat.ts +73 -0
  44. package/packages/subagents/src/custom-agents.ts +10 -2
  45. package/packages/subagents/src/index.ts +14 -3
  46. package/packages/subagents/src/types.ts +2 -0
  47. package/packages/unipi/bundled.js +786 -312
  48. package/packages/updater/package.json +2 -2
  49. package/packages/utility/README.md +9 -0
  50. package/packages/utility/package.json +2 -2
  51. package/packages/utility/src/index.ts +48 -0
  52. package/packages/utility/src/lifecycle/cleanup.ts +29 -0
  53. package/packages/utility/src/prefix-cache.ts +263 -0
  54. package/packages/utility/src/types.ts +1 -1
  55. package/packages/web-api/package.json +2 -2
  56. package/packages/workflow/index.ts +2 -2
  57. 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).filter((f) => f.endsWith(".md"));
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).filter((f) => f.endsWith(".md"));
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
- (record.result?.trim() || "No output."),
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
- output += record.result?.trim() || "No output.";
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;