@getpaseo/server 0.1.91-beta.2 → 0.1.92
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/dist/scripts/supervisor.js +21 -0
- package/dist/server/server/agent/agent-manager.d.ts +13 -5
- package/dist/server/server/agent/agent-manager.js +110 -74
- package/dist/server/server/agent/agent-projections.d.ts +4 -2
- package/dist/server/server/agent/agent-projections.js +8 -28
- package/dist/server/server/agent/agent-sdk-types.d.ts +30 -10
- package/dist/server/server/agent/import-sessions.d.ts +3 -2
- package/dist/server/server/agent/import-sessions.js +23 -55
- package/dist/server/server/agent/prompt-attachments.js +8 -0
- package/dist/server/server/agent/provider-registry.d.ts +0 -1
- package/dist/server/server/agent/provider-registry.js +55 -16
- package/dist/server/server/agent/provider-session-import.d.ts +10 -0
- package/dist/server/server/agent/provider-session-import.js +49 -0
- package/dist/server/server/agent/providers/acp-agent.d.ts +12 -2
- package/dist/server/server/agent/providers/acp-agent.js +78 -36
- package/dist/server/server/agent/providers/claude/agent.d.ts +3 -2
- package/dist/server/server/agent/providers/claude/agent.js +28 -24
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +3 -2
- package/dist/server/server/agent/providers/codex-app-server-agent.js +29 -26
- package/dist/server/server/agent/providers/cursor-acp-agent.d.ts +1 -0
- package/dist/server/server/agent/providers/cursor-acp-agent.js +1 -0
- package/dist/server/server/agent/providers/generic-acp-agent.d.ts +9 -0
- package/dist/server/server/agent/providers/generic-acp-agent.js +18 -1
- package/dist/server/server/agent/providers/mock-load-test-agent.d.ts +3 -2
- package/dist/server/server/agent/providers/mock-load-test-agent.js +11 -1
- package/dist/server/server/agent/providers/mock-slow-provider.d.ts +1 -2
- package/dist/server/server/agent/providers/mock-slow-provider.js +0 -3
- package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.d.ts +3 -0
- package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.js +12 -0
- package/dist/server/server/agent/providers/opencode-agent.d.ts +18 -3
- package/dist/server/server/agent/providers/opencode-agent.js +135 -36
- package/dist/server/server/agent/providers/pi/agent.d.ts +25 -2
- package/dist/server/server/agent/providers/pi/agent.js +243 -14
- package/dist/server/server/agent/providers/pi/cli-runtime.js +9 -0
- package/dist/server/server/agent/providers/pi/rpc-types.d.ts +9 -0
- package/dist/server/server/agent/providers/pi/runtime.d.ts +2 -0
- package/dist/server/server/agent/providers/pi/session-descriptor.d.ts +11 -0
- package/dist/server/server/agent/providers/pi/session-descriptor.js +284 -0
- package/dist/server/server/agent/providers/pi/test-utils/fake-pi.d.ts +8 -0
- package/dist/server/server/agent/providers/pi/test-utils/fake-pi.js +22 -0
- package/dist/server/server/agent/runtime-mcp-config.d.ts +8 -0
- package/dist/server/server/agent/runtime-mcp-config.js +50 -0
- package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +2 -2
- package/dist/server/server/daemon-worker.js +84 -1
- package/dist/server/server/file-upload/index.d.ts +27 -0
- package/dist/server/server/file-upload/index.js +158 -0
- package/dist/server/server/loop-service.d.ts +12 -12
- package/dist/server/server/persisted-config.d.ts +8 -0
- package/dist/server/server/persisted-config.js +1 -1
- package/dist/server/server/persistence-hooks.js +6 -4
- package/dist/server/server/session.d.ts +5 -2
- package/dist/server/server/session.js +20 -3
- package/dist/server/server/speech/providers/local/runtime.js +1 -0
- package/dist/server/server/speech/providers/local/worker-client.d.ts +14 -1
- package/dist/server/server/speech/providers/local/worker-client.js +169 -7
- package/dist/server/server/websocket-server.d.ts +2 -0
- package/dist/server/server/websocket-server.js +20 -7
- package/dist/server/server/workspace-registry.d.ts +4 -4
- package/dist/src/server/persisted-config.js +1 -1
- package/package.json +5 -5
|
@@ -2,6 +2,8 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { importSessionFromPersistence } from "../../provider-session-import.js";
|
|
5
7
|
import { runProviderTurn } from "../provider-runner.js";
|
|
6
8
|
import { checkProviderLaunchAvailable, resolveProviderLaunch, } from "../../provider-launch-config.js";
|
|
7
9
|
import { renderPromptAttachmentAsText } from "../../prompt-attachments.js";
|
|
@@ -10,6 +12,7 @@ import { buildBinaryDiagnosticRows, formatDiagnosticStatus, formatProviderDiagno
|
|
|
10
12
|
import { getUserMessageText, streamPiHistory, } from "./history-mapper.js";
|
|
11
13
|
import { PiCliRuntime } from "./cli-runtime.js";
|
|
12
14
|
import { revertPiConversation } from "./rewind.js";
|
|
15
|
+
import { listPiImportableSessions } from "./session-descriptor.js";
|
|
13
16
|
import { mapToolDetail, parseToolArgs, parseToolResult, resolveToolCallName, } from "./tool-call-mapper.js";
|
|
14
17
|
const PI_PROVIDER = "pi";
|
|
15
18
|
const DEFAULT_PI_THINKING_LEVEL = "medium";
|
|
@@ -23,9 +26,35 @@ const QUESTION_RESPONSE_HEADER = "Response";
|
|
|
23
26
|
const QUESTION_COMMENT_HEADER = "Comment";
|
|
24
27
|
const PI_ASK_USER_FREEFORM_SENTINEL = "✏️ Type custom response...";
|
|
25
28
|
const COMBINED_ASK_USER_METADATA = "ask_user_select_optional_comment";
|
|
29
|
+
export const PiProviderParamsSchema = z
|
|
30
|
+
.object({
|
|
31
|
+
sessionDir: z.string().min(1).optional(),
|
|
32
|
+
})
|
|
33
|
+
.strict();
|
|
34
|
+
const PI_HANDLED_BUILTIN_SLASH_COMMANDS = [
|
|
35
|
+
{
|
|
36
|
+
name: "compact",
|
|
37
|
+
description: "Manually compact the session context",
|
|
38
|
+
argumentHint: "[instructions]",
|
|
39
|
+
kind: "command",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "autocompact",
|
|
43
|
+
description: "Toggle automatic context compaction",
|
|
44
|
+
argumentHint: "[on|off|toggle]",
|
|
45
|
+
kind: "command",
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
function mapPiCommandKind(source) {
|
|
49
|
+
if (source === "skill") {
|
|
50
|
+
return "skill";
|
|
51
|
+
}
|
|
52
|
+
return "command";
|
|
53
|
+
}
|
|
26
54
|
const PI_CAPABILITIES = {
|
|
27
55
|
supportsStreaming: true,
|
|
28
56
|
supportsSessionPersistence: true,
|
|
57
|
+
supportsSessionListing: true,
|
|
29
58
|
supportsDynamicModes: true,
|
|
30
59
|
supportsMcpServers: false,
|
|
31
60
|
supportsReasoningStream: true,
|
|
@@ -43,7 +72,12 @@ const PI_THINKING_OPTIONS = [
|
|
|
43
72
|
{ id: "xhigh", label: "XHigh", description: "Maximum reasoning" },
|
|
44
73
|
];
|
|
45
74
|
function normalizePiModelLabel(label) {
|
|
46
|
-
|
|
75
|
+
const normalizedLabel = label.trim().replace(/[_\s]+/g, " ");
|
|
76
|
+
const vendorSeparatorIndex = normalizedLabel.indexOf(": ");
|
|
77
|
+
if (vendorSeparatorIndex === -1) {
|
|
78
|
+
return normalizedLabel;
|
|
79
|
+
}
|
|
80
|
+
return normalizedLabel.slice(vendorSeparatorIndex + 2).trim();
|
|
47
81
|
}
|
|
48
82
|
export function transformPiModels(models) {
|
|
49
83
|
return models.map((model) => {
|
|
@@ -76,6 +110,19 @@ function normalizePiThinkingOption(value) {
|
|
|
76
110
|
}
|
|
77
111
|
return isPiThinkingLevel(value) ? value : null;
|
|
78
112
|
}
|
|
113
|
+
function parseAutoCompactMode(value) {
|
|
114
|
+
const mode = (value ?? "toggle").trim().toLowerCase();
|
|
115
|
+
if (mode === "on" || mode === "true" || mode === "enable" || mode === "enabled") {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (mode === "off" || mode === "false" || mode === "disable" || mode === "disabled") {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (mode === "toggle") {
|
|
122
|
+
return "toggle";
|
|
123
|
+
}
|
|
124
|
+
return "unknown";
|
|
125
|
+
}
|
|
79
126
|
function mapThinkingOption(option) {
|
|
80
127
|
const mappedOption = {
|
|
81
128
|
id: option.id,
|
|
@@ -654,6 +701,9 @@ export class PiRpcAgentSession {
|
|
|
654
701
|
this.seenUserEntryIds = new Set();
|
|
655
702
|
this.pendingUserMessages = [];
|
|
656
703
|
this.pendingExtensionResults = new Map();
|
|
704
|
+
this.outOfBandCompactionEmit = null;
|
|
705
|
+
this.outOfBandCompactionStarted = false;
|
|
706
|
+
this.outOfBandCompactionCompleted = false;
|
|
657
707
|
this.closed = false;
|
|
658
708
|
this.runtimeSession = options.runtimeSession;
|
|
659
709
|
this.config = options.config;
|
|
@@ -821,11 +871,42 @@ export class PiRpcAgentSession {
|
|
|
821
871
|
}
|
|
822
872
|
async listCommands() {
|
|
823
873
|
const commands = await this.runtimeSession.getCommands();
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
874
|
+
const mappedCommands = new Map(PI_HANDLED_BUILTIN_SLASH_COMMANDS.map((command) => [command.name, { ...command }]));
|
|
875
|
+
for (const command of commands) {
|
|
876
|
+
const knownCommand = mappedCommands.get(command.name);
|
|
877
|
+
mappedCommands.set(command.name, {
|
|
878
|
+
name: command.name,
|
|
879
|
+
description: command.description ?? command.source,
|
|
880
|
+
argumentHint: knownCommand?.argumentHint ?? "",
|
|
881
|
+
kind: mapPiCommandKind(command.source),
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
return [...mappedCommands.values()];
|
|
885
|
+
}
|
|
886
|
+
tryHandleOutOfBand(prompt) {
|
|
887
|
+
if (typeof prompt !== "string") {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
const parsed = this.parseSlashCommandInput(prompt);
|
|
891
|
+
if (!parsed) {
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
const commandName = parsed.commandName.toLowerCase();
|
|
895
|
+
if (commandName === "compact") {
|
|
896
|
+
return {
|
|
897
|
+
run: async ({ emit }) => {
|
|
898
|
+
await this.executeCompactCommand(parsed.args, emit);
|
|
899
|
+
},
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
if (commandName === "autocompact") {
|
|
903
|
+
return {
|
|
904
|
+
run: async ({ emit }) => {
|
|
905
|
+
await this.executeAutoCompactCommand(parsed.args, emit);
|
|
906
|
+
},
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
return null;
|
|
829
910
|
}
|
|
830
911
|
async setModel(modelId) {
|
|
831
912
|
const parsedReference = parseModelReference(modelId);
|
|
@@ -860,6 +941,117 @@ export class PiRpcAgentSession {
|
|
|
860
941
|
currentTurnIdForEvent() {
|
|
861
942
|
return this.activeTurnId ?? undefined;
|
|
862
943
|
}
|
|
944
|
+
parseSlashCommandInput(text) {
|
|
945
|
+
const trimmed = text.trim();
|
|
946
|
+
if (!trimmed.startsWith("/") || trimmed.length <= 1) {
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
const withoutPrefix = trimmed.slice(1);
|
|
950
|
+
const firstWhitespaceIdx = withoutPrefix.search(/\s/);
|
|
951
|
+
const commandName = firstWhitespaceIdx === -1 ? withoutPrefix : withoutPrefix.slice(0, firstWhitespaceIdx);
|
|
952
|
+
if (!commandName || commandName.includes("/")) {
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
const rawArgs = firstWhitespaceIdx === -1 ? "" : withoutPrefix.slice(firstWhitespaceIdx + 1).trim();
|
|
956
|
+
return rawArgs.length > 0 ? { commandName, args: rawArgs } : { commandName };
|
|
957
|
+
}
|
|
958
|
+
async executeCompactCommand(customInstructions, emit) {
|
|
959
|
+
if (this.outOfBandCompactionEmit) {
|
|
960
|
+
throw new Error("A Pi compact command is already running");
|
|
961
|
+
}
|
|
962
|
+
this.outOfBandCompactionEmit = emit;
|
|
963
|
+
this.outOfBandCompactionStarted = false;
|
|
964
|
+
this.outOfBandCompactionCompleted = false;
|
|
965
|
+
try {
|
|
966
|
+
await this.runtimeSession.compact(customInstructions);
|
|
967
|
+
}
|
|
968
|
+
catch (error) {
|
|
969
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
970
|
+
if (this.outOfBandCompactionEmit === emit &&
|
|
971
|
+
this.outOfBandCompactionStarted &&
|
|
972
|
+
!this.outOfBandCompactionCompleted) {
|
|
973
|
+
this.emitCompactionTimeline({
|
|
974
|
+
turnId: undefined,
|
|
975
|
+
item: {
|
|
976
|
+
type: "compaction",
|
|
977
|
+
status: "completed",
|
|
978
|
+
trigger: "manual",
|
|
979
|
+
},
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
emit({
|
|
983
|
+
type: "timeline",
|
|
984
|
+
provider: PI_PROVIDER,
|
|
985
|
+
item: {
|
|
986
|
+
type: "assistant_message",
|
|
987
|
+
text: `[Error] Failed to compact context: ${message}`,
|
|
988
|
+
},
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
finally {
|
|
992
|
+
if (this.outOfBandCompactionEmit === emit && !this.outOfBandCompactionStarted) {
|
|
993
|
+
this.outOfBandCompactionEmit = null;
|
|
994
|
+
this.outOfBandCompactionStarted = false;
|
|
995
|
+
this.outOfBandCompactionCompleted = false;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
async executeAutoCompactCommand(mode, emit) {
|
|
1000
|
+
let enabled = parseAutoCompactMode(mode);
|
|
1001
|
+
if (enabled === "unknown") {
|
|
1002
|
+
emit({
|
|
1003
|
+
type: "timeline",
|
|
1004
|
+
provider: PI_PROVIDER,
|
|
1005
|
+
item: {
|
|
1006
|
+
type: "assistant_message",
|
|
1007
|
+
text: "[Error] Usage: /autocompact [on|off|toggle]",
|
|
1008
|
+
},
|
|
1009
|
+
});
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
if (enabled === "toggle") {
|
|
1013
|
+
const state = await this.runtimeSession.getState();
|
|
1014
|
+
if (typeof state.autoCompactionEnabled !== "boolean") {
|
|
1015
|
+
emit({
|
|
1016
|
+
type: "timeline",
|
|
1017
|
+
provider: PI_PROVIDER,
|
|
1018
|
+
item: {
|
|
1019
|
+
type: "assistant_message",
|
|
1020
|
+
text: "[Error] Auto-compaction state is unavailable. Use /autocompact on or /autocompact off.",
|
|
1021
|
+
},
|
|
1022
|
+
});
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
enabled = !state.autoCompactionEnabled;
|
|
1026
|
+
}
|
|
1027
|
+
try {
|
|
1028
|
+
await this.runtimeSession.setAutoCompaction(enabled);
|
|
1029
|
+
}
|
|
1030
|
+
catch (error) {
|
|
1031
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1032
|
+
emit({
|
|
1033
|
+
type: "timeline",
|
|
1034
|
+
provider: PI_PROVIDER,
|
|
1035
|
+
item: {
|
|
1036
|
+
type: "assistant_message",
|
|
1037
|
+
text: `[Error] Failed to set auto-compaction: ${message}`,
|
|
1038
|
+
},
|
|
1039
|
+
});
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
this.state = {
|
|
1043
|
+
...this.state,
|
|
1044
|
+
autoCompactionEnabled: enabled,
|
|
1045
|
+
};
|
|
1046
|
+
emit({
|
|
1047
|
+
type: "timeline",
|
|
1048
|
+
provider: PI_PROVIDER,
|
|
1049
|
+
item: {
|
|
1050
|
+
type: "assistant_message",
|
|
1051
|
+
text: `Auto-compaction ${enabled ? "enabled" : "disabled"}.`,
|
|
1052
|
+
},
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
863
1055
|
async requestEntryCapture(reason) {
|
|
864
1056
|
const requestId = randomUUID();
|
|
865
1057
|
const resultPromise = this.waitForExtensionResult(requestId);
|
|
@@ -1090,9 +1282,7 @@ export class PiRpcAgentSession {
|
|
|
1090
1282
|
return;
|
|
1091
1283
|
}
|
|
1092
1284
|
case "compaction_start":
|
|
1093
|
-
this.
|
|
1094
|
-
type: "timeline",
|
|
1095
|
-
provider: PI_PROVIDER,
|
|
1285
|
+
this.emitCompactionTimeline({
|
|
1096
1286
|
turnId,
|
|
1097
1287
|
item: {
|
|
1098
1288
|
type: "compaction",
|
|
@@ -1102,13 +1292,12 @@ export class PiRpcAgentSession {
|
|
|
1102
1292
|
});
|
|
1103
1293
|
return;
|
|
1104
1294
|
case "compaction_end":
|
|
1105
|
-
this.
|
|
1106
|
-
type: "timeline",
|
|
1107
|
-
provider: PI_PROVIDER,
|
|
1295
|
+
this.emitCompactionTimeline({
|
|
1108
1296
|
turnId,
|
|
1109
1297
|
item: {
|
|
1110
1298
|
type: "compaction",
|
|
1111
1299
|
status: "completed",
|
|
1300
|
+
trigger: event.reason === "manual" ? "manual" : "auto",
|
|
1112
1301
|
},
|
|
1113
1302
|
});
|
|
1114
1303
|
return;
|
|
@@ -1119,6 +1308,33 @@ export class PiRpcAgentSession {
|
|
|
1119
1308
|
return;
|
|
1120
1309
|
}
|
|
1121
1310
|
}
|
|
1311
|
+
emitCompactionTimeline(input) {
|
|
1312
|
+
const emitOutOfBand = this.outOfBandCompactionEmit;
|
|
1313
|
+
if (emitOutOfBand && input.item.type === "compaction") {
|
|
1314
|
+
if (input.item.status === "loading") {
|
|
1315
|
+
this.outOfBandCompactionStarted = true;
|
|
1316
|
+
}
|
|
1317
|
+
if (input.item.status === "completed") {
|
|
1318
|
+
this.outOfBandCompactionCompleted = true;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
const event = {
|
|
1322
|
+
type: "timeline",
|
|
1323
|
+
provider: PI_PROVIDER,
|
|
1324
|
+
...(emitOutOfBand ? {} : { turnId: input.turnId }),
|
|
1325
|
+
item: input.item,
|
|
1326
|
+
};
|
|
1327
|
+
if (emitOutOfBand) {
|
|
1328
|
+
emitOutOfBand(event);
|
|
1329
|
+
if (input.item.type === "compaction" && input.item.status === "completed") {
|
|
1330
|
+
this.outOfBandCompactionEmit = null;
|
|
1331
|
+
this.outOfBandCompactionStarted = false;
|
|
1332
|
+
this.outOfBandCompactionCompleted = false;
|
|
1333
|
+
}
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
this.emit(event);
|
|
1337
|
+
}
|
|
1122
1338
|
handleMessageUpdate(event, turnId) {
|
|
1123
1339
|
if (event.message.role !== "assistant") {
|
|
1124
1340
|
return;
|
|
@@ -1240,6 +1456,7 @@ export class PiRpcAgentClient {
|
|
|
1240
1456
|
this.capabilities = PI_CAPABILITIES;
|
|
1241
1457
|
this.logger = options.logger;
|
|
1242
1458
|
this.runtimeSettings = options.runtimeSettings;
|
|
1459
|
+
this.providerParams = PiProviderParamsSchema.parse(options.providerParams ?? {});
|
|
1243
1460
|
this.runtime = options.runtime ?? createRuntime(options.logger, options.runtimeSettings);
|
|
1244
1461
|
}
|
|
1245
1462
|
async createSession(config, launchContext) {
|
|
@@ -1332,8 +1549,20 @@ export class PiRpcAgentClient {
|
|
|
1332
1549
|
async listModes(_options) {
|
|
1333
1550
|
return [];
|
|
1334
1551
|
}
|
|
1335
|
-
async
|
|
1336
|
-
return
|
|
1552
|
+
async listImportableSessions(options) {
|
|
1553
|
+
return await listPiImportableSessions({
|
|
1554
|
+
...options,
|
|
1555
|
+
sessionDir: this.providerParams.sessionDir,
|
|
1556
|
+
runtimeSettings: this.runtimeSettings,
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
async importSession(input, context) {
|
|
1560
|
+
return importSessionFromPersistence({
|
|
1561
|
+
provider: PI_PROVIDER,
|
|
1562
|
+
request: input,
|
|
1563
|
+
context,
|
|
1564
|
+
resumeSession: this.resumeSession.bind(this),
|
|
1565
|
+
});
|
|
1337
1566
|
}
|
|
1338
1567
|
async isAvailable() {
|
|
1339
1568
|
const launch = await this.resolvePiLaunch();
|
|
@@ -76,6 +76,15 @@ class PiCliRuntimeSession {
|
|
|
76
76
|
async prompt(message, images) {
|
|
77
77
|
await this.request({ type: "prompt", message, ...(images?.length ? { images } : {}) });
|
|
78
78
|
}
|
|
79
|
+
async compact(customInstructions) {
|
|
80
|
+
await this.request({
|
|
81
|
+
type: "compact",
|
|
82
|
+
...(customInstructions ? { customInstructions } : {}),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async setAutoCompaction(enabled) {
|
|
86
|
+
await this.request({ type: "set_auto_compaction", enabled });
|
|
87
|
+
}
|
|
79
88
|
async abort() {
|
|
80
89
|
await this.request({ type: "abort" });
|
|
81
90
|
}
|
|
@@ -66,6 +66,7 @@ export interface PiSessionState {
|
|
|
66
66
|
thinkingLevel: PiThinkingLevel;
|
|
67
67
|
isStreaming: boolean;
|
|
68
68
|
isCompacting: boolean;
|
|
69
|
+
autoCompactionEnabled?: boolean;
|
|
69
70
|
sessionFile?: string;
|
|
70
71
|
sessionId: string;
|
|
71
72
|
sessionName?: string;
|
|
@@ -98,6 +99,14 @@ export type PiRpcCommand = {
|
|
|
98
99
|
type: "prompt";
|
|
99
100
|
message: string;
|
|
100
101
|
images?: PiImageContent[];
|
|
102
|
+
} | {
|
|
103
|
+
id?: string;
|
|
104
|
+
type: "compact";
|
|
105
|
+
customInstructions?: string;
|
|
106
|
+
} | {
|
|
107
|
+
id?: string;
|
|
108
|
+
type: "set_auto_compaction";
|
|
109
|
+
enabled: boolean;
|
|
101
110
|
} | {
|
|
102
111
|
id?: string;
|
|
103
112
|
type: "abort";
|
|
@@ -28,6 +28,8 @@ export interface PiRuntimeSession {
|
|
|
28
28
|
data: string;
|
|
29
29
|
mimeType: string;
|
|
30
30
|
}>): Promise<void>;
|
|
31
|
+
compact(customInstructions?: string): Promise<void>;
|
|
32
|
+
setAutoCompaction(enabled: boolean): Promise<void>;
|
|
31
33
|
abort(): Promise<void>;
|
|
32
34
|
getState(): Promise<PiSessionState>;
|
|
33
35
|
getMessages(): Promise<PiAgentMessage[]>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ImportableProviderSession, ListImportableSessionsOptions } from "../../agent-sdk-types.js";
|
|
2
|
+
import type { ProviderRuntimeSettings } from "../../provider-launch-config.js";
|
|
3
|
+
interface PiSessionDescriptorOptions extends ListImportableSessionsOptions {
|
|
4
|
+
sessionDir?: string;
|
|
5
|
+
runtimeSettings?: ProviderRuntimeSettings;
|
|
6
|
+
env?: NodeJS.ProcessEnv;
|
|
7
|
+
homeDir?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function listPiImportableSessions(options?: PiSessionDescriptorOptions): Promise<ImportableProviderSession[]>;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=session-descriptor.d.ts.map
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { open, readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createRealpathAwarePathMatcher } from "../../../../utils/path.js";
|
|
5
|
+
const PI_CONFIG_DIR_NAME = ".pi";
|
|
6
|
+
const PI_AGENT_DIR_ENV = "PI_CODING_AGENT_DIR";
|
|
7
|
+
const PI_SESSION_DIR_ENV = "PI_CODING_AGENT_SESSION_DIR";
|
|
8
|
+
const HEAD_BYTES = 64 * 1024;
|
|
9
|
+
const TAIL_BYTES = 256 * 1024;
|
|
10
|
+
const FULL_SCAN_LINE_LIMIT = 2000;
|
|
11
|
+
export async function listPiImportableSessions(options = {}) {
|
|
12
|
+
const sessionsDir = await resolvePiSessionsDir(options);
|
|
13
|
+
const files = await walkJsonlFiles(sessionsDir);
|
|
14
|
+
const matchesCwd = options.cwd ? createRealpathAwarePathMatcher(options.cwd) : null;
|
|
15
|
+
const limit = options.limit ?? 20;
|
|
16
|
+
const sessions = [];
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
const session = await readPiImportableSession(file);
|
|
19
|
+
if (!session)
|
|
20
|
+
continue;
|
|
21
|
+
if (matchesCwd && !matchesCwd(session.cwd))
|
|
22
|
+
continue;
|
|
23
|
+
sessions.push(session);
|
|
24
|
+
}
|
|
25
|
+
return sessions
|
|
26
|
+
.sort((left, right) => right.lastActivityAt.getTime() - left.lastActivityAt.getTime())
|
|
27
|
+
.slice(0, limit);
|
|
28
|
+
}
|
|
29
|
+
async function resolvePiSessionsDir(options) {
|
|
30
|
+
const env = options.env ?? process.env;
|
|
31
|
+
const homeDir = options.homeDir ?? homedir();
|
|
32
|
+
const baseDir = options.cwd ?? process.cwd();
|
|
33
|
+
if (options.sessionDir?.trim()) {
|
|
34
|
+
return resolveConfigPath(options.sessionDir, { baseDir, homeDir });
|
|
35
|
+
}
|
|
36
|
+
const agentDir = resolvePiAgentDir({ runtimeSettings: options.runtimeSettings, env, homeDir });
|
|
37
|
+
const envSessionDir = options.runtimeSettings?.env?.[PI_SESSION_DIR_ENV] ?? env[PI_SESSION_DIR_ENV];
|
|
38
|
+
if (envSessionDir?.trim()) {
|
|
39
|
+
return resolveConfigPath(envSessionDir, { baseDir, homeDir });
|
|
40
|
+
}
|
|
41
|
+
const settingsSessionDir = await readConfiguredSessionDir({
|
|
42
|
+
agentDir,
|
|
43
|
+
cwd: options.cwd,
|
|
44
|
+
});
|
|
45
|
+
if (settingsSessionDir?.trim()) {
|
|
46
|
+
return resolveConfigPath(settingsSessionDir, { baseDir, homeDir });
|
|
47
|
+
}
|
|
48
|
+
return path.join(agentDir, "sessions");
|
|
49
|
+
}
|
|
50
|
+
function resolvePiAgentDir(input) {
|
|
51
|
+
const configured = input.runtimeSettings?.env?.[PI_AGENT_DIR_ENV] ?? input.env[PI_AGENT_DIR_ENV];
|
|
52
|
+
if (configured?.trim()) {
|
|
53
|
+
return resolveConfigPath(configured, { baseDir: process.cwd(), homeDir: input.homeDir });
|
|
54
|
+
}
|
|
55
|
+
return path.join(input.homeDir, PI_CONFIG_DIR_NAME, "agent");
|
|
56
|
+
}
|
|
57
|
+
async function readConfiguredSessionDir(input) {
|
|
58
|
+
const values = await Promise.all([
|
|
59
|
+
readSessionDirFromSettings(path.join(input.agentDir, "settings.json")),
|
|
60
|
+
input.cwd
|
|
61
|
+
? readSessionDirFromSettings(path.join(input.cwd, PI_CONFIG_DIR_NAME, "settings.json"))
|
|
62
|
+
: null,
|
|
63
|
+
]);
|
|
64
|
+
return values[1] ?? values[0] ?? null;
|
|
65
|
+
}
|
|
66
|
+
async function readSessionDirFromSettings(settingsPath) {
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(await readFile(settingsPath, "utf8"));
|
|
69
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const sessionDir = Reflect.get(parsed, "sessionDir");
|
|
73
|
+
return typeof sessionDir === "string" && sessionDir.trim() ? sessionDir : null;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function resolveConfigPath(value, options) {
|
|
80
|
+
if (value === "~") {
|
|
81
|
+
return options.homeDir;
|
|
82
|
+
}
|
|
83
|
+
if (value.startsWith("~/")) {
|
|
84
|
+
return path.join(options.homeDir, value.slice(2));
|
|
85
|
+
}
|
|
86
|
+
return path.isAbsolute(value) ? value : path.resolve(options.baseDir, value);
|
|
87
|
+
}
|
|
88
|
+
async function walkJsonlFiles(root) {
|
|
89
|
+
let entries;
|
|
90
|
+
try {
|
|
91
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const files = await Promise.all(entries.map(async (entry) => {
|
|
97
|
+
const entryPath = path.join(root, entry.name);
|
|
98
|
+
if (entry.isDirectory()) {
|
|
99
|
+
return await walkJsonlFiles(entryPath);
|
|
100
|
+
}
|
|
101
|
+
return entry.isFile() && entry.name.endsWith(".jsonl") ? [entryPath] : [];
|
|
102
|
+
}));
|
|
103
|
+
return files.flat();
|
|
104
|
+
}
|
|
105
|
+
async function readPiImportableSession(filePath) {
|
|
106
|
+
const firstLine = await readFirstLine(filePath);
|
|
107
|
+
if (!firstLine)
|
|
108
|
+
return null;
|
|
109
|
+
const header = parseSessionHeader(firstLine);
|
|
110
|
+
if (!header)
|
|
111
|
+
return null;
|
|
112
|
+
const tail = await readTail(filePath).catch(() => "");
|
|
113
|
+
const tailInfo = parseSessionTail(tail);
|
|
114
|
+
const headInfo = await scanSessionHead(filePath);
|
|
115
|
+
const title = tailInfo.title ?? headInfo.title ?? headInfo.firstUserMessage;
|
|
116
|
+
const lastActivityAt = tailInfo.lastActivityAt ?? (await readFileMtime(filePath)) ?? header.createdAt ?? new Date(0);
|
|
117
|
+
return {
|
|
118
|
+
providerHandleId: filePath,
|
|
119
|
+
cwd: header.cwd,
|
|
120
|
+
title,
|
|
121
|
+
firstPromptPreview: normalizePromptPreview(headInfo.firstUserMessage),
|
|
122
|
+
lastPromptPreview: normalizePromptPreview(tailInfo.lastUserMessage ?? headInfo.firstUserMessage),
|
|
123
|
+
lastActivityAt,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
async function readFirstLine(filePath) {
|
|
127
|
+
const handle = await open(filePath, "r").catch(() => null);
|
|
128
|
+
if (!handle)
|
|
129
|
+
return null;
|
|
130
|
+
try {
|
|
131
|
+
const buffer = Buffer.alloc(HEAD_BYTES);
|
|
132
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
133
|
+
if (bytesRead <= 0)
|
|
134
|
+
return null;
|
|
135
|
+
const chunk = buffer.subarray(0, bytesRead).toString("utf8");
|
|
136
|
+
const newlineIndex = chunk.indexOf("\n");
|
|
137
|
+
return (newlineIndex === -1 ? chunk : chunk.slice(0, newlineIndex)).trim();
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
await handle.close().catch(() => undefined);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function readTail(filePath) {
|
|
144
|
+
const fileStats = await stat(filePath);
|
|
145
|
+
const start = Math.max(0, fileStats.size - TAIL_BYTES);
|
|
146
|
+
const length = fileStats.size - start;
|
|
147
|
+
const handle = await open(filePath, "r");
|
|
148
|
+
try {
|
|
149
|
+
const buffer = Buffer.alloc(length);
|
|
150
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, start);
|
|
151
|
+
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
await handle.close().catch(() => undefined);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function readFileMtime(filePath) {
|
|
158
|
+
try {
|
|
159
|
+
return (await stat(filePath)).mtime;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function parseSessionHeader(firstLine) {
|
|
166
|
+
const entry = parseJsonRecord(firstLine);
|
|
167
|
+
if (!entry || entry.type !== "session")
|
|
168
|
+
return null;
|
|
169
|
+
const sessionId = typeof entry.id === "string" ? entry.id : null;
|
|
170
|
+
const cwd = typeof entry.cwd === "string" ? entry.cwd : null;
|
|
171
|
+
if (!sessionId || !cwd)
|
|
172
|
+
return null;
|
|
173
|
+
const createdAt = parseDate(entry.timestamp);
|
|
174
|
+
return { sessionId, cwd, createdAt };
|
|
175
|
+
}
|
|
176
|
+
function parseSessionTail(tail) {
|
|
177
|
+
const lines = tail.split(/\r?\n/u);
|
|
178
|
+
let title = null;
|
|
179
|
+
let lastActivityAt = null;
|
|
180
|
+
let fallbackTimestamp = null;
|
|
181
|
+
let lastUserMessage = null;
|
|
182
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
183
|
+
const entry = parseJsonRecord(lines[index].trim());
|
|
184
|
+
if (!entry)
|
|
185
|
+
continue;
|
|
186
|
+
if (!title && entry.type === "session_info") {
|
|
187
|
+
title = readNonEmptyString(entry.name);
|
|
188
|
+
}
|
|
189
|
+
const entryTimestamp = parseDate(entry.timestamp);
|
|
190
|
+
if (!fallbackTimestamp && entryTimestamp) {
|
|
191
|
+
fallbackTimestamp = entryTimestamp;
|
|
192
|
+
}
|
|
193
|
+
if (entry.type !== "message") {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (!lastActivityAt && entryTimestamp) {
|
|
197
|
+
lastActivityAt = entryTimestamp;
|
|
198
|
+
}
|
|
199
|
+
if (!lastUserMessage && isRecord(entry.message) && entry.message.role === "user") {
|
|
200
|
+
lastUserMessage = extractMessageText(entry.message.content);
|
|
201
|
+
}
|
|
202
|
+
if (title && lastActivityAt && lastUserMessage) {
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { title, lastActivityAt: lastActivityAt ?? fallbackTimestamp, lastUserMessage };
|
|
207
|
+
}
|
|
208
|
+
async function scanSessionHead(filePath) {
|
|
209
|
+
let content;
|
|
210
|
+
try {
|
|
211
|
+
content = await readFile(filePath, "utf8");
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return { title: null, firstUserMessage: null };
|
|
215
|
+
}
|
|
216
|
+
let title = null;
|
|
217
|
+
let firstUserMessage = null;
|
|
218
|
+
let lineCount = 0;
|
|
219
|
+
for (const rawLine of content.split(/\r?\n/u)) {
|
|
220
|
+
lineCount += 1;
|
|
221
|
+
const entry = parseJsonRecord(rawLine.trim());
|
|
222
|
+
if (!entry)
|
|
223
|
+
continue;
|
|
224
|
+
if (entry.type === "session_info") {
|
|
225
|
+
title = readNonEmptyString(entry.name) ?? title;
|
|
226
|
+
}
|
|
227
|
+
if (!firstUserMessage && entry.type === "message" && isRecord(entry.message)) {
|
|
228
|
+
if (entry.message.role === "user") {
|
|
229
|
+
firstUserMessage = extractMessageText(entry.message.content);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (title && firstUserMessage) {
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
if (lineCount >= FULL_SCAN_LINE_LIMIT && firstUserMessage) {
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return { title, firstUserMessage };
|
|
240
|
+
}
|
|
241
|
+
function parseJsonRecord(line) {
|
|
242
|
+
if (!line)
|
|
243
|
+
return null;
|
|
244
|
+
try {
|
|
245
|
+
const parsed = JSON.parse(line);
|
|
246
|
+
return isRecord(parsed) ? parsed : null;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function isRecord(value) {
|
|
253
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
254
|
+
}
|
|
255
|
+
function readNonEmptyString(value) {
|
|
256
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
257
|
+
}
|
|
258
|
+
function normalizePromptPreview(text) {
|
|
259
|
+
const normalized = text?.trim().replace(/\s+/g, " ") ?? "";
|
|
260
|
+
if (!normalized)
|
|
261
|
+
return null;
|
|
262
|
+
return normalized.length > 160 ? normalized.slice(0, 160) : normalized;
|
|
263
|
+
}
|
|
264
|
+
function parseDate(value) {
|
|
265
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
const date = new Date(value);
|
|
269
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
270
|
+
}
|
|
271
|
+
function extractMessageText(content) {
|
|
272
|
+
if (typeof content === "string") {
|
|
273
|
+
return content.trim() || null;
|
|
274
|
+
}
|
|
275
|
+
if (!Array.isArray(content)) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
const text = content
|
|
279
|
+
.flatMap((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : [])
|
|
280
|
+
.join("\n\n")
|
|
281
|
+
.trim();
|
|
282
|
+
return text || null;
|
|
283
|
+
}
|
|
284
|
+
//# sourceMappingURL=session-descriptor.js.map
|