@andreroggeri/adapter-pi-local-serialized 0.1.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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +75 -0
  3. package/dist/cli/format-event.d.ts +2 -0
  4. package/dist/cli/format-event.d.ts.map +1 -0
  5. package/dist/cli/format-event.js +99 -0
  6. package/dist/cli/format-event.js.map +1 -0
  7. package/dist/cli/index.d.ts +2 -0
  8. package/dist/cli/index.d.ts.map +1 -0
  9. package/dist/cli/index.js +2 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/index.d.ts +11 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +49 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/server/concurrency.d.ts +2 -0
  16. package/dist/server/concurrency.d.ts.map +1 -0
  17. package/dist/server/concurrency.js +25 -0
  18. package/dist/server/concurrency.js.map +1 -0
  19. package/dist/server/execute.d.ts +3 -0
  20. package/dist/server/execute.d.ts.map +1 -0
  21. package/dist/server/execute.js +675 -0
  22. package/dist/server/execute.js.map +1 -0
  23. package/dist/server/execute.remote.test.d.ts +2 -0
  24. package/dist/server/execute.remote.test.d.ts.map +1 -0
  25. package/dist/server/execute.remote.test.js +466 -0
  26. package/dist/server/execute.remote.test.js.map +1 -0
  27. package/dist/server/index.d.ts +8 -0
  28. package/dist/server/index.d.ts.map +1 -0
  29. package/dist/server/index.js +51 -0
  30. package/dist/server/index.js.map +1 -0
  31. package/dist/server/models.d.ts +20 -0
  32. package/dist/server/models.d.ts.map +1 -0
  33. package/dist/server/models.js +163 -0
  34. package/dist/server/models.js.map +1 -0
  35. package/dist/server/models.test.d.ts +2 -0
  36. package/dist/server/models.test.d.ts.map +1 -0
  37. package/dist/server/models.test.js +22 -0
  38. package/dist/server/models.test.js.map +1 -0
  39. package/dist/server/parse.d.ts +23 -0
  40. package/dist/server/parse.d.ts.map +1 -0
  41. package/dist/server/parse.js +195 -0
  42. package/dist/server/parse.js.map +1 -0
  43. package/dist/server/parse.test.d.ts +2 -0
  44. package/dist/server/parse.test.d.ts.map +1 -0
  45. package/dist/server/parse.test.js +249 -0
  46. package/dist/server/parse.test.js.map +1 -0
  47. package/dist/server/skills.d.ts +8 -0
  48. package/dist/server/skills.d.ts.map +1 -0
  49. package/dist/server/skills.js +69 -0
  50. package/dist/server/skills.js.map +1 -0
  51. package/dist/server/test.d.ts +3 -0
  52. package/dist/server/test.d.ts.map +1 -0
  53. package/dist/server/test.js +309 -0
  54. package/dist/server/test.js.map +1 -0
  55. package/dist/ui/build-config.d.ts +3 -0
  56. package/dist/ui/build-config.d.ts.map +1 -0
  57. package/dist/ui/build-config.js +78 -0
  58. package/dist/ui/build-config.js.map +1 -0
  59. package/dist/ui/index.d.ts +3 -0
  60. package/dist/ui/index.d.ts.map +1 -0
  61. package/dist/ui/index.js +3 -0
  62. package/dist/ui/index.js.map +1 -0
  63. package/dist/ui/parse-stdout.d.ts +4 -0
  64. package/dist/ui/parse-stdout.d.ts.map +1 -0
  65. package/dist/ui/parse-stdout.js +271 -0
  66. package/dist/ui/parse-stdout.js.map +1 -0
  67. package/package.json +54 -0
@@ -0,0 +1,675 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { inferOpenAiCompatibleBiller } from "@paperclipai/adapter-utils";
6
+ import { adapterExecutionTargetIsRemote, adapterExecutionTargetRemoteCwd, overrideAdapterExecutionTargetRemoteCwd, adapterExecutionTargetSessionIdentity, adapterExecutionTargetSessionMatches, adapterExecutionTargetUsesManagedHome, adapterExecutionTargetUsesPaperclipBridge, describeAdapterExecutionTarget, ensureAdapterExecutionTargetCommandResolvable, ensureAdapterExecutionTargetFile, ensureAdapterExecutionTargetRuntimeCommandInstalled, prepareAdapterExecutionTargetRuntime, readAdapterExecutionTarget, resolveAdapterExecutionTargetTimeoutSec, resolveAdapterExecutionTargetCommandForLogs, runAdapterExecutionTargetProcess, runAdapterExecutionTargetShellCommand, startAdapterExecutionTargetPaperclipBridge, } from "@paperclipai/adapter-utils/execution-target";
7
+ import { asString, asNumber, asStringArray, parseObject, buildPaperclipEnv, joinPromptSections, buildInvocationEnvForLogs, ensureAbsoluteDirectory, ensurePaperclipSkillSymlink, ensurePathInEnv, refreshPaperclipWorkspaceEnvForExecution, readPaperclipRuntimeSkillEntries, readPaperclipIssueWorkModeFromContext, resolvePaperclipDesiredSkillNames, removeMaintainerOnlySkillSymlinks, renderTemplate, renderPaperclipWakePrompt, stringifyPaperclipWakePayload, DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE, } from "@paperclipai/adapter-utils/server-utils";
8
+ import { shellQuote } from "@paperclipai/adapter-utils/ssh";
9
+ import { isPiUnknownSessionError, parsePiJsonl } from "./parse.js";
10
+ import { ensurePiModelConfiguredAndAvailable } from "./models.js";
11
+ import { withSerializedExecution } from "./concurrency.js";
12
+ import { SANDBOX_INSTALL_COMMAND } from "../index.js";
13
+ const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
14
+ const PAPERCLIP_SESSIONS_DIR = path.join(os.homedir(), ".pi", "paperclips");
15
+ const PI_AGENT_SKILLS_DIR = path.join(os.homedir(), ".pi", "agent", "skills");
16
+ function firstNonEmptyLine(text) {
17
+ return (text
18
+ .split(/\r?\n/)
19
+ .map((line) => line.trim())
20
+ .find(Boolean) ?? "");
21
+ }
22
+ function parseModelProvider(model) {
23
+ if (!model)
24
+ return null;
25
+ const trimmed = model.trim();
26
+ if (!trimmed.includes("/"))
27
+ return null;
28
+ return trimmed.slice(0, trimmed.indexOf("/")).trim() || null;
29
+ }
30
+ function parseModelId(model) {
31
+ if (!model)
32
+ return null;
33
+ const trimmed = model.trim();
34
+ if (!trimmed.includes("/"))
35
+ return trimmed || null;
36
+ return trimmed.slice(trimmed.indexOf("/") + 1).trim() || null;
37
+ }
38
+ async function ensurePiSkillsInjected(onLog, skillsEntries, desiredSkillNames) {
39
+ const desiredSet = new Set(desiredSkillNames ?? skillsEntries.map((entry) => entry.key));
40
+ const selectedEntries = skillsEntries.filter((entry) => desiredSet.has(entry.key));
41
+ if (selectedEntries.length === 0)
42
+ return;
43
+ await fs.mkdir(PI_AGENT_SKILLS_DIR, { recursive: true });
44
+ const removedSkills = await removeMaintainerOnlySkillSymlinks(PI_AGENT_SKILLS_DIR, selectedEntries.map((entry) => entry.runtimeName));
45
+ for (const skillName of removedSkills) {
46
+ await onLog("stderr", `[paperclip] Removed maintainer-only Pi skill "${skillName}" from ${PI_AGENT_SKILLS_DIR}\n`);
47
+ }
48
+ for (const entry of selectedEntries) {
49
+ const target = path.join(PI_AGENT_SKILLS_DIR, entry.runtimeName);
50
+ try {
51
+ const result = await ensurePaperclipSkillSymlink(entry.source, target);
52
+ if (result === "skipped")
53
+ continue;
54
+ await onLog("stderr", `[paperclip] ${result === "repaired" ? "Repaired" : "Injected"} Pi skill "${entry.runtimeName}" into ${PI_AGENT_SKILLS_DIR}\n`);
55
+ }
56
+ catch (err) {
57
+ await onLog("stderr", `[paperclip] Failed to inject Pi skill "${entry.runtimeName}" into ${PI_AGENT_SKILLS_DIR}: ${err instanceof Error ? err.message : String(err)}\n`);
58
+ }
59
+ }
60
+ }
61
+ async function buildPiSkillsDir(config) {
62
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-pi-skills-"));
63
+ const target = path.join(tmp, "skills");
64
+ await fs.mkdir(target, { recursive: true });
65
+ const availableEntries = await readPaperclipRuntimeSkillEntries(config, __moduleDir);
66
+ const desiredNames = new Set(resolvePaperclipDesiredSkillNames(config, availableEntries));
67
+ for (const entry of availableEntries) {
68
+ if (!desiredNames.has(entry.key))
69
+ continue;
70
+ await fs.symlink(entry.source, path.join(target, entry.runtimeName));
71
+ }
72
+ return target;
73
+ }
74
+ function resolvePiBiller(env, provider) {
75
+ return inferOpenAiCompatibleBiller(env, null) ?? provider ?? "unknown";
76
+ }
77
+ async function ensureSessionsDir() {
78
+ await fs.mkdir(PAPERCLIP_SESSIONS_DIR, { recursive: true });
79
+ return PAPERCLIP_SESSIONS_DIR;
80
+ }
81
+ function buildSessionPath(agentId, timestamp) {
82
+ const safeTimestamp = timestamp.replace(/[:.]/g, "-");
83
+ return path.join(PAPERCLIP_SESSIONS_DIR, `${safeTimestamp}-${agentId}.jsonl`);
84
+ }
85
+ function buildRemoteSessionPath(runtimeRootDir, agentId, timestamp) {
86
+ const safeTimestamp = timestamp.replace(/[:.]/g, "-");
87
+ return path.posix.join(runtimeRootDir, "sessions", `${safeTimestamp}-${agentId}.jsonl`);
88
+ }
89
+ function normalizeExecutionCwd(candidate, remote) {
90
+ return remote ? path.posix.normalize(candidate) : path.resolve(candidate);
91
+ }
92
+ function executionCwdsMatch(saved, current, remote) {
93
+ return normalizeExecutionCwd(saved, remote) === normalizeExecutionCwd(current, remote);
94
+ }
95
+ function readSessionHeaderCwd(raw) {
96
+ const headerLine = raw
97
+ .split(/\r?\n/)
98
+ .map((line) => line.trim())
99
+ .find(Boolean);
100
+ if (!headerLine)
101
+ return null;
102
+ try {
103
+ const parsed = JSON.parse(headerLine);
104
+ if (parsed.type !== "session")
105
+ return null;
106
+ const cwd = typeof parsed.cwd === "string" ? parsed.cwd.trim() : "";
107
+ return cwd.length > 0 ? cwd : null;
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ }
113
+ async function readSavedSessionCwd(input) {
114
+ if (!input.sessionPath.trim())
115
+ return null;
116
+ if (!adapterExecutionTargetIsRemote(input.executionTarget)) {
117
+ try {
118
+ return readSessionHeaderCwd(await fs.readFile(input.sessionPath, "utf8"));
119
+ }
120
+ catch {
121
+ return null;
122
+ }
123
+ }
124
+ try {
125
+ const sessionHeader = await runAdapterExecutionTargetShellCommand(input.runId, input.executionTarget, `if [ -f ${shellQuote(input.sessionPath)} ]; then head -n 1 ${shellQuote(input.sessionPath)}; fi`, {
126
+ cwd: input.cwd,
127
+ env: input.env,
128
+ timeoutSec: input.timeoutSec > 0 ? Math.min(input.timeoutSec, 15) : 15,
129
+ graceSec: input.graceSec,
130
+ });
131
+ if (sessionHeader.timedOut || (sessionHeader.exitCode ?? 0) !== 0)
132
+ return null;
133
+ return readSessionHeaderCwd(sessionHeader.stdout);
134
+ }
135
+ catch {
136
+ return null;
137
+ }
138
+ }
139
+ export async function execute(ctx) {
140
+ const { runId, agent, runtime, config, context, onLog, onMeta, onSpawn, authToken } = ctx;
141
+ const executionTarget = readAdapterExecutionTarget({
142
+ executionTarget: ctx.executionTarget,
143
+ legacyRemoteExecution: ctx.executionTransport?.remoteExecution,
144
+ });
145
+ const executionTargetIsRemote = adapterExecutionTargetIsRemote(executionTarget);
146
+ const promptTemplate = asString(config.promptTemplate, DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE);
147
+ const command = asString(config.command, "pi");
148
+ const model = asString(config.model, "").trim();
149
+ const thinking = asString(config.thinking, "").trim();
150
+ // Parse model into provider and model id
151
+ const provider = parseModelProvider(model);
152
+ const modelId = parseModelId(model);
153
+ const workspaceContext = parseObject(context.paperclipWorkspace);
154
+ const workspaceCwd = asString(workspaceContext.cwd, "");
155
+ const workspaceSource = asString(workspaceContext.source, "");
156
+ const workspaceId = asString(workspaceContext.workspaceId, "");
157
+ const workspaceRepoUrl = asString(workspaceContext.repoUrl, "");
158
+ const workspaceRepoRef = asString(workspaceContext.repoRef, "");
159
+ const agentHome = asString(workspaceContext.agentHome, "");
160
+ const workspaceHints = Array.isArray(context.paperclipWorkspaces)
161
+ ? context.paperclipWorkspaces.filter((value) => typeof value === "object" && value !== null)
162
+ : [];
163
+ const configuredCwd = asString(config.cwd, "");
164
+ const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd.length > 0;
165
+ const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd;
166
+ const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd();
167
+ let effectiveExecutionCwd = adapterExecutionTargetRemoteCwd(executionTarget, cwd);
168
+ await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
169
+ if (!executionTargetIsRemote) {
170
+ await ensureSessionsDir();
171
+ }
172
+ const piSkillEntries = await readPaperclipRuntimeSkillEntries(config, __moduleDir);
173
+ const desiredPiSkillNames = resolvePaperclipDesiredSkillNames(config, piSkillEntries);
174
+ if (!executionTargetIsRemote) {
175
+ await ensurePiSkillsInjected(onLog, piSkillEntries, desiredPiSkillNames);
176
+ }
177
+ // Build environment
178
+ const envConfig = parseObject(config.env);
179
+ const hasExplicitApiKey = typeof envConfig.PAPERCLIP_API_KEY === "string" && envConfig.PAPERCLIP_API_KEY.trim().length > 0;
180
+ const env = { ...buildPaperclipEnv(agent) };
181
+ env.PAPERCLIP_RUN_ID = runId;
182
+ const wakeTaskId = (typeof context.taskId === "string" && context.taskId.trim().length > 0 && context.taskId.trim()) ||
183
+ (typeof context.issueId === "string" && context.issueId.trim().length > 0 && context.issueId.trim()) ||
184
+ null;
185
+ const wakeReason = typeof context.wakeReason === "string" && context.wakeReason.trim().length > 0
186
+ ? context.wakeReason.trim()
187
+ : null;
188
+ const wakeCommentId = (typeof context.wakeCommentId === "string" && context.wakeCommentId.trim().length > 0 && context.wakeCommentId.trim()) ||
189
+ (typeof context.commentId === "string" && context.commentId.trim().length > 0 && context.commentId.trim()) ||
190
+ null;
191
+ const approvalId = typeof context.approvalId === "string" && context.approvalId.trim().length > 0
192
+ ? context.approvalId.trim()
193
+ : null;
194
+ const approvalStatus = typeof context.approvalStatus === "string" && context.approvalStatus.trim().length > 0
195
+ ? context.approvalStatus.trim()
196
+ : null;
197
+ const linkedIssueIds = Array.isArray(context.issueIds)
198
+ ? context.issueIds.filter((value) => typeof value === "string" && value.trim().length > 0)
199
+ : [];
200
+ const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
201
+ const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
202
+ if (wakeTaskId)
203
+ env.PAPERCLIP_TASK_ID = wakeTaskId;
204
+ if (issueWorkMode)
205
+ env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode;
206
+ if (wakeReason)
207
+ env.PAPERCLIP_WAKE_REASON = wakeReason;
208
+ if (wakeCommentId)
209
+ env.PAPERCLIP_WAKE_COMMENT_ID = wakeCommentId;
210
+ if (approvalId)
211
+ env.PAPERCLIP_APPROVAL_ID = approvalId;
212
+ if (approvalStatus)
213
+ env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
214
+ if (linkedIssueIds.length > 0)
215
+ env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
216
+ if (wakePayloadJson)
217
+ env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
218
+ refreshPaperclipWorkspaceEnvForExecution({
219
+ env,
220
+ envConfig,
221
+ workspaceCwd: effectiveWorkspaceCwd,
222
+ workspaceSource,
223
+ workspaceId,
224
+ workspaceRepoUrl,
225
+ workspaceRepoRef,
226
+ workspaceHints,
227
+ agentHome,
228
+ executionTargetIsRemote,
229
+ executionCwd: effectiveExecutionCwd,
230
+ });
231
+ if (!hasExplicitApiKey && authToken) {
232
+ env.PAPERCLIP_API_KEY = authToken;
233
+ }
234
+ // Prepend installed skill `bin/` dirs to PATH so an agent's bash tool can
235
+ // invoke skill binaries (e.g. `paperclip-get-issue`) by name. Without this,
236
+ // any pi_local agent whose AGENTS.md calls a skill command via bash hits
237
+ // exit 127 "command not found". Only include skills that ensurePiSkillsInjected
238
+ // actually linked — otherwise non-injected skills' binaries would be reachable
239
+ // to the agent.
240
+ const injectedSkillKeys = new Set(desiredPiSkillNames);
241
+ const skillBinDirs = piSkillEntries
242
+ .filter((entry) => injectedSkillKeys.has(entry.key) && entry.source.length > 0)
243
+ .map((entry) => path.join(entry.source, "bin"));
244
+ const mergedEnv = ensurePathInEnv({ ...process.env, ...env });
245
+ const pathKey = typeof mergedEnv.Path === "string" && mergedEnv.Path.length > 0 && !mergedEnv.PATH
246
+ ? "Path"
247
+ : "PATH";
248
+ const basePath = mergedEnv[pathKey] ?? "";
249
+ if (skillBinDirs.length > 0) {
250
+ const existing = basePath.split(path.delimiter).filter(Boolean);
251
+ const additions = skillBinDirs.filter((dir) => !existing.includes(dir));
252
+ if (additions.length > 0) {
253
+ mergedEnv[pathKey] = [...additions, basePath].filter(Boolean).join(path.delimiter);
254
+ }
255
+ }
256
+ const runtimeEnv = Object.fromEntries(Object.entries(mergedEnv).filter((entry) => typeof entry[1] === "string"));
257
+ const timeoutSec = resolveAdapterExecutionTargetTimeoutSec(executionTarget, asNumber(config.timeoutSec, 0));
258
+ const graceSec = asNumber(config.graceSec, 20);
259
+ await ensureAdapterExecutionTargetRuntimeCommandInstalled({
260
+ runId,
261
+ target: executionTarget,
262
+ installCommand: ctx.runtimeCommandSpec?.installCommand,
263
+ detectCommand: ctx.runtimeCommandSpec?.detectCommand,
264
+ cwd,
265
+ env: runtimeEnv,
266
+ timeoutSec,
267
+ graceSec,
268
+ onLog,
269
+ });
270
+ await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv, {
271
+ installCommand: SANDBOX_INSTALL_COMMAND,
272
+ timeoutSec,
273
+ });
274
+ const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv);
275
+ let loggedEnv = buildInvocationEnvForLogs(env, {
276
+ runtimeEnv,
277
+ includeRuntimeKeys: ["HOME"],
278
+ resolvedCommand,
279
+ });
280
+ if (!executionTargetIsRemote) {
281
+ await ensurePiModelConfiguredAndAvailable({
282
+ model,
283
+ command,
284
+ cwd,
285
+ env: runtimeEnv,
286
+ });
287
+ }
288
+ const extraArgs = (() => {
289
+ const fromExtraArgs = asStringArray(config.extraArgs);
290
+ if (fromExtraArgs.length > 0)
291
+ return fromExtraArgs;
292
+ return asStringArray(config.args);
293
+ })();
294
+ let restoreRemoteWorkspace = null;
295
+ let remoteRuntimeRootDir = null;
296
+ let localSkillsDir = null;
297
+ let remoteSkillsDir = null;
298
+ let paperclipBridge = null;
299
+ if (executionTargetIsRemote) {
300
+ try {
301
+ localSkillsDir = await buildPiSkillsDir(config);
302
+ await onLog("stdout", `[paperclip] Syncing workspace and Pi runtime assets to ${describeAdapterExecutionTarget(executionTarget)}.\n`);
303
+ const preparedRemoteRuntime = await prepareAdapterExecutionTargetRuntime({
304
+ runId,
305
+ target: executionTarget,
306
+ adapterKey: "pi",
307
+ timeoutSec,
308
+ workspaceLocalDir: cwd,
309
+ installCommand: SANDBOX_INSTALL_COMMAND,
310
+ detectCommand: command,
311
+ assets: [
312
+ {
313
+ key: "skills",
314
+ localDir: localSkillsDir,
315
+ followSymlinks: true,
316
+ },
317
+ ],
318
+ });
319
+ restoreRemoteWorkspace = () => preparedRemoteRuntime.restoreWorkspace();
320
+ effectiveExecutionCwd = preparedRemoteRuntime.workspaceRemoteDir ?? effectiveExecutionCwd;
321
+ refreshPaperclipWorkspaceEnvForExecution({
322
+ env,
323
+ envConfig,
324
+ workspaceCwd: effectiveWorkspaceCwd,
325
+ workspaceSource,
326
+ workspaceId,
327
+ workspaceRepoUrl,
328
+ workspaceRepoRef,
329
+ workspaceHints,
330
+ agentHome,
331
+ executionTargetIsRemote,
332
+ executionCwd: effectiveExecutionCwd,
333
+ });
334
+ if (adapterExecutionTargetUsesManagedHome(executionTarget) && preparedRemoteRuntime.runtimeRootDir) {
335
+ env.HOME = preparedRemoteRuntime.runtimeRootDir;
336
+ }
337
+ remoteRuntimeRootDir = preparedRemoteRuntime.runtimeRootDir;
338
+ remoteSkillsDir = preparedRemoteRuntime.assetDirs.skills ?? null;
339
+ }
340
+ catch (error) {
341
+ await Promise.allSettled([
342
+ restoreRemoteWorkspace?.(),
343
+ localSkillsDir ? fs.rm(path.dirname(localSkillsDir), { recursive: true, force: true }).catch(() => undefined) : Promise.resolve(),
344
+ ]);
345
+ throw error;
346
+ }
347
+ }
348
+ const runtimeExecutionTarget = overrideAdapterExecutionTargetRemoteCwd(executionTarget, effectiveExecutionCwd);
349
+ if (executionTargetIsRemote && adapterExecutionTargetUsesPaperclipBridge(runtimeExecutionTarget)) {
350
+ paperclipBridge = await startAdapterExecutionTargetPaperclipBridge({
351
+ runId,
352
+ target: runtimeExecutionTarget,
353
+ runtimeRootDir: remoteRuntimeRootDir,
354
+ adapterKey: "pi",
355
+ timeoutSec,
356
+ hostApiToken: env.PAPERCLIP_API_KEY,
357
+ onLog,
358
+ });
359
+ if (paperclipBridge) {
360
+ Object.assign(env, paperclipBridge.env);
361
+ loggedEnv = buildInvocationEnvForLogs(env, {
362
+ runtimeEnv: Object.fromEntries(Object.entries(ensurePathInEnv({ ...process.env, ...env })).filter((entry) => typeof entry[1] === "string")),
363
+ includeRuntimeKeys: ["HOME"],
364
+ resolvedCommand,
365
+ });
366
+ }
367
+ }
368
+ const runtimeSessionParams = parseObject(runtime.sessionParams);
369
+ const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? "");
370
+ const runtimeSessionCwd = asString(runtimeSessionParams.cwd, "");
371
+ const runtimeRemoteExecution = parseObject(runtimeSessionParams.remoteExecution);
372
+ const sessionTargetMatches = adapterExecutionTargetSessionMatches(runtimeRemoteExecution, runtimeExecutionTarget);
373
+ const sessionParamsCwdMatches = runtimeSessionCwd.length === 0 ||
374
+ executionCwdsMatch(runtimeSessionCwd, effectiveExecutionCwd, executionTargetIsRemote);
375
+ const savedSessionCwd = runtimeSessionId.length > 0
376
+ ? await readSavedSessionCwd({
377
+ runId,
378
+ sessionPath: runtimeSessionId,
379
+ executionTarget: runtimeExecutionTarget ?? null,
380
+ cwd,
381
+ env,
382
+ timeoutSec,
383
+ graceSec,
384
+ })
385
+ : null;
386
+ const sessionHeaderCwdMatches = runtimeSessionId.length === 0 ||
387
+ (savedSessionCwd !== null &&
388
+ executionCwdsMatch(savedSessionCwd, effectiveExecutionCwd, executionTargetIsRemote));
389
+ const canResumeSession = runtimeSessionId.length > 0 &&
390
+ sessionTargetMatches &&
391
+ sessionParamsCwdMatches &&
392
+ sessionHeaderCwdMatches;
393
+ const sessionPath = canResumeSession
394
+ ? runtimeSessionId
395
+ : executionTargetIsRemote && remoteRuntimeRootDir
396
+ ? buildRemoteSessionPath(remoteRuntimeRootDir, agent.id, new Date().toISOString())
397
+ : buildSessionPath(agent.id, new Date().toISOString());
398
+ if (runtimeSessionId && !canResumeSession) {
399
+ const staleSessionCwdNote = savedSessionCwd !== null && !sessionHeaderCwdMatches
400
+ ? ` Pi stored cwd "${savedSessionCwd}" in the session header, so Paperclip will start a fresh session for "${effectiveExecutionCwd}".`
401
+ : "";
402
+ await onLog("stdout", executionTargetIsRemote
403
+ ? `[paperclip] Pi session "${runtimeSessionId}" does not match the current remote execution state and will not be resumed in "${effectiveExecutionCwd}".${staleSessionCwdNote} Starting a fresh remote session.\n`
404
+ : `[paperclip] Pi session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${effectiveExecutionCwd}".${staleSessionCwdNote}\n`);
405
+ }
406
+ if (!canResumeSession) {
407
+ if (executionTargetIsRemote) {
408
+ await ensureAdapterExecutionTargetFile(runId, runtimeExecutionTarget, sessionPath, {
409
+ cwd,
410
+ env,
411
+ timeoutSec: 15,
412
+ graceSec: 5,
413
+ onLog,
414
+ });
415
+ }
416
+ else {
417
+ try {
418
+ await fs.writeFile(sessionPath, "", { flag: "wx" });
419
+ }
420
+ catch (err) {
421
+ if (err.code !== "EEXIST") {
422
+ throw err;
423
+ }
424
+ }
425
+ }
426
+ }
427
+ // Handle instructions file and build system prompt extension
428
+ const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
429
+ const resolvedInstructionsFilePath = instructionsFilePath
430
+ ? path.resolve(cwd, instructionsFilePath)
431
+ : "";
432
+ const instructionsFileDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : "";
433
+ let systemPromptExtension = "";
434
+ let instructionsReadFailed = false;
435
+ if (resolvedInstructionsFilePath) {
436
+ try {
437
+ const instructionsContents = await fs.readFile(resolvedInstructionsFilePath, "utf8");
438
+ systemPromptExtension =
439
+ `${instructionsContents}\n\n` +
440
+ `The above agent instructions were loaded from ${resolvedInstructionsFilePath}. ` +
441
+ `Resolve any relative file references from ${instructionsFileDir}.\n\n` +
442
+ DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE;
443
+ }
444
+ catch (err) {
445
+ instructionsReadFailed = true;
446
+ const reason = err instanceof Error ? err.message : String(err);
447
+ await onLog("stdout", `[paperclip] Warning: could not read agent instructions file "${resolvedInstructionsFilePath}": ${reason}\n`);
448
+ // Fall back to base prompt template
449
+ systemPromptExtension = promptTemplate;
450
+ }
451
+ }
452
+ else {
453
+ systemPromptExtension = promptTemplate;
454
+ }
455
+ const bootstrapPromptTemplate = asString(config.bootstrapPromptTemplate, "");
456
+ const templateData = {
457
+ agentId: agent.id,
458
+ companyId: agent.companyId,
459
+ runId,
460
+ company: { id: agent.companyId },
461
+ agent,
462
+ run: { id: runId, source: "on_demand" },
463
+ context,
464
+ };
465
+ const renderedSystemPromptExtension = renderTemplate(systemPromptExtension, templateData);
466
+ const renderedBootstrapPrompt = !canResumeSession && bootstrapPromptTemplate.trim().length > 0
467
+ ? renderTemplate(bootstrapPromptTemplate, templateData).trim()
468
+ : "";
469
+ const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: canResumeSession });
470
+ const shouldUseResumeDeltaPrompt = canResumeSession && wakePrompt.length > 0;
471
+ const renderedHeartbeatPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
472
+ const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
473
+ const userPrompt = joinPromptSections([
474
+ renderedBootstrapPrompt,
475
+ wakePrompt,
476
+ sessionHandoffNote,
477
+ renderedHeartbeatPrompt,
478
+ ]);
479
+ const promptMetrics = {
480
+ systemPromptChars: renderedSystemPromptExtension.length,
481
+ promptChars: userPrompt.length,
482
+ bootstrapPromptChars: renderedBootstrapPrompt.length,
483
+ wakePromptChars: wakePrompt.length,
484
+ sessionHandoffChars: sessionHandoffNote.length,
485
+ heartbeatPromptChars: renderedHeartbeatPrompt.length,
486
+ };
487
+ const commandNotes = (() => {
488
+ if (!resolvedInstructionsFilePath)
489
+ return [];
490
+ if (instructionsReadFailed) {
491
+ return [
492
+ `Configured instructionsFilePath ${resolvedInstructionsFilePath}, but file could not be read; continuing without injected instructions.`,
493
+ ];
494
+ }
495
+ return [
496
+ `Loaded agent instructions from ${resolvedInstructionsFilePath}`,
497
+ `Appended instructions + path directive to system prompt (relative references from ${instructionsFileDir}).`,
498
+ ];
499
+ })();
500
+ const buildArgs = (sessionFile) => {
501
+ const args = [];
502
+ // Use JSON mode for structured output with print mode (non-interactive)
503
+ args.push("--mode", "json");
504
+ args.push("-p"); // Non-interactive mode: process prompt and exit
505
+ // Use --append-system-prompt to extend Pi's default system prompt
506
+ args.push("--append-system-prompt", renderedSystemPromptExtension);
507
+ if (provider)
508
+ args.push("--provider", provider);
509
+ if (modelId)
510
+ args.push("--model", modelId);
511
+ if (thinking)
512
+ args.push("--thinking", thinking);
513
+ args.push("--tools", "read,bash,edit,write,grep,find,ls");
514
+ args.push("--session", sessionFile);
515
+ args.push("--skill", remoteSkillsDir ?? PI_AGENT_SKILLS_DIR);
516
+ if (extraArgs.length > 0)
517
+ args.push(...extraArgs);
518
+ // Add the user prompt as the last argument
519
+ args.push(userPrompt);
520
+ return args;
521
+ };
522
+ const runAttempt = async (sessionFile) => {
523
+ const args = buildArgs(sessionFile);
524
+ if (onMeta) {
525
+ await onMeta({
526
+ adapterType: "pi_local",
527
+ command: resolvedCommand,
528
+ cwd: effectiveExecutionCwd,
529
+ commandNotes,
530
+ commandArgs: args,
531
+ env: loggedEnv,
532
+ prompt: userPrompt,
533
+ promptMetrics,
534
+ context,
535
+ });
536
+ }
537
+ // Buffer stdout by lines to handle partial JSON chunks
538
+ let stdoutBuffer = "";
539
+ const bufferedOnLog = async (stream, chunk) => {
540
+ if (stream === "stderr") {
541
+ // Pass stderr through immediately (not JSONL)
542
+ await onLog(stream, chunk);
543
+ return;
544
+ }
545
+ // Buffer stdout and emit only complete lines
546
+ stdoutBuffer += chunk;
547
+ const lines = stdoutBuffer.split("\n");
548
+ // Keep the last (potentially incomplete) line in the buffer
549
+ stdoutBuffer = lines.pop() || "";
550
+ // Emit complete lines
551
+ for (const line of lines) {
552
+ if (line) {
553
+ await onLog(stream, line + "\n");
554
+ }
555
+ }
556
+ };
557
+ const proc = await runAdapterExecutionTargetProcess(runId, runtimeExecutionTarget, command, args, {
558
+ cwd,
559
+ env: executionTargetIsRemote ? env : runtimeEnv,
560
+ timeoutSec,
561
+ graceSec,
562
+ onSpawn,
563
+ onLog: bufferedOnLog,
564
+ });
565
+ // Flush any remaining buffer content
566
+ if (stdoutBuffer) {
567
+ await onLog("stdout", stdoutBuffer);
568
+ }
569
+ return {
570
+ proc,
571
+ rawStderr: proc.stderr,
572
+ parsed: parsePiJsonl(proc.stdout),
573
+ };
574
+ };
575
+ const toResult = (attempt, clearSessionOnMissingSession = false) => {
576
+ if (attempt.proc.timedOut) {
577
+ return {
578
+ exitCode: attempt.proc.exitCode,
579
+ signal: attempt.proc.signal,
580
+ timedOut: true,
581
+ errorMessage: `Timed out after ${timeoutSec}s`,
582
+ clearSession: clearSessionOnMissingSession,
583
+ };
584
+ }
585
+ const resolvedSessionId = clearSessionOnMissingSession ? null : sessionPath;
586
+ const resolvedSessionParams = resolvedSessionId
587
+ ? {
588
+ sessionId: resolvedSessionId,
589
+ cwd: effectiveExecutionCwd,
590
+ ...(workspaceId ? { workspaceId } : {}),
591
+ ...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}),
592
+ ...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}),
593
+ ...(executionTargetIsRemote
594
+ ? {
595
+ remoteExecution: adapterExecutionTargetSessionIdentity(runtimeExecutionTarget),
596
+ }
597
+ : {}),
598
+ }
599
+ : null;
600
+ const stderrLine = firstNonEmptyLine(attempt.proc.stderr);
601
+ const rawExitCode = attempt.proc.exitCode;
602
+ const parsedError = attempt.parsed.errors.find((error) => error.trim().length > 0) ?? "";
603
+ const effectiveExitCode = (rawExitCode ?? 0) === 0 && parsedError ? 1 : rawExitCode;
604
+ const fallbackErrorMessage = parsedError || stderrLine || `Pi exited with code ${rawExitCode ?? -1}`;
605
+ return {
606
+ exitCode: effectiveExitCode,
607
+ signal: attempt.proc.signal,
608
+ timedOut: false,
609
+ errorMessage: (effectiveExitCode ?? 0) === 0 ? null : fallbackErrorMessage,
610
+ usage: {
611
+ inputTokens: attempt.parsed.usage.inputTokens,
612
+ outputTokens: attempt.parsed.usage.outputTokens,
613
+ cachedInputTokens: attempt.parsed.usage.cachedInputTokens,
614
+ },
615
+ sessionId: resolvedSessionId,
616
+ sessionParams: resolvedSessionParams,
617
+ sessionDisplayId: resolvedSessionId,
618
+ provider: provider,
619
+ biller: resolvePiBiller(runtimeEnv, provider),
620
+ model: model,
621
+ billingType: "unknown",
622
+ costUsd: attempt.parsed.usage.costUsd,
623
+ resultJson: {
624
+ stdout: attempt.proc.stdout,
625
+ stderr: attempt.proc.stderr,
626
+ },
627
+ summary: attempt.parsed.finalMessage ?? attempt.parsed.messages.join("\n\n").trim(),
628
+ clearSession: Boolean(clearSessionOnMissingSession),
629
+ };
630
+ };
631
+ try {
632
+ return await withSerializedExecution(agent.id, onLog, async () => {
633
+ const initial = await runAttempt(sessionPath);
634
+ const initialFailed = !initial.proc.timedOut && ((initial.proc.exitCode ?? 0) !== 0 || initial.parsed.errors.length > 0);
635
+ if (canResumeSession &&
636
+ initialFailed &&
637
+ isPiUnknownSessionError(initial.proc.stdout, initial.rawStderr)) {
638
+ await onLog("stdout", `[paperclip] Pi session "${runtimeSessionId}" is unavailable; retrying with a fresh session.\n`);
639
+ const newSessionPath = executionTargetIsRemote && remoteRuntimeRootDir
640
+ ? buildRemoteSessionPath(remoteRuntimeRootDir, agent.id, new Date().toISOString())
641
+ : buildSessionPath(agent.id, new Date().toISOString());
642
+ if (executionTargetIsRemote) {
643
+ await ensureAdapterExecutionTargetFile(runId, executionTarget, newSessionPath, {
644
+ cwd,
645
+ env,
646
+ timeoutSec: 15,
647
+ graceSec: 5,
648
+ onLog,
649
+ });
650
+ }
651
+ else {
652
+ try {
653
+ await fs.writeFile(newSessionPath, "", { flag: "wx" });
654
+ }
655
+ catch (err) {
656
+ if (err.code !== "EEXIST") {
657
+ throw err;
658
+ }
659
+ }
660
+ }
661
+ const retry = await runAttempt(newSessionPath);
662
+ return toResult(retry, true);
663
+ }
664
+ return toResult(initial);
665
+ });
666
+ }
667
+ finally {
668
+ await Promise.all([
669
+ paperclipBridge?.stop(),
670
+ restoreRemoteWorkspace?.(),
671
+ localSkillsDir ? fs.rm(path.dirname(localSkillsDir), { recursive: true, force: true }).catch(() => undefined) : Promise.resolve(),
672
+ ]);
673
+ }
674
+ }
675
+ //# sourceMappingURL=execute.js.map