fix: synchronize chat backend contracts and ownership
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
de08048d99
|
|
@ -161,6 +161,7 @@ async function runExecutor(
|
|||
config: Record<string, unknown>,
|
||||
options: {
|
||||
context?: Record<string, unknown>;
|
||||
runtime?: Record<string, unknown>;
|
||||
executionTransport?: Record<string, unknown>;
|
||||
authToken?: string;
|
||||
executionTarget?: Record<string, unknown>;
|
||||
|
|
@ -194,7 +195,7 @@ async function runExecutor(
|
|||
id: "agent-1",
|
||||
companyId: "company-1",
|
||||
},
|
||||
runtime: {},
|
||||
runtime: options.runtime ?? {},
|
||||
config,
|
||||
context: options.context ?? {},
|
||||
executionTransport: options.executionTransport,
|
||||
|
|
@ -592,6 +593,52 @@ describe("shared ACPX engine runtime behavior", () => {
|
|||
expect(promptMetrics?.runtimeNoteChars).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["claude", false], ["codex", false], ["claude", true], ["codex", true],
|
||||
] as const)("keeps %s ACP conversation policy on fresh, resumed, and reset turns (custom=%s)", async (agent, custom) => {
|
||||
const root = await makeTempRoot();
|
||||
const config = { agent, cwd: root, stateDir: path.join(root, "state"), mode: "persistent",
|
||||
...(custom ? { promptTemplate: "Custom agent instructions." } : {}),
|
||||
};
|
||||
const chatDirective = "Chat mode: clarify goals and hand accepted plans off to ordinary project tasks.";
|
||||
const context = {
|
||||
conversationMode: true,
|
||||
taskId: "chat-1",
|
||||
paperclipTaskMarkdown: chatDirective,
|
||||
paperclipTaskMarkdownCompact: chatDirective,
|
||||
paperclipWake: {
|
||||
reason: "issue_commented",
|
||||
issue: { id: "chat-1", workMode: "planning", status: "in_progress" },
|
||||
interactionKind: "request_confirmation",
|
||||
interactionStatus: "accepted",
|
||||
comments: [],
|
||||
commentWindow: { requestedCount: 0, includedCount: 0, missingCount: 0 },
|
||||
fallbackFetchNeeded: false,
|
||||
},
|
||||
};
|
||||
const fresh = await runExecutor(config, { context });
|
||||
const resumed = await runExecutor(config, {
|
||||
context,
|
||||
runtime: { sessionParams: fresh.result.sessionParams },
|
||||
});
|
||||
expect(resumed.sessionInputs[0]?.resumeSessionId).toBe(fresh.result.sessionId);
|
||||
const reset = await runExecutor(config, { context });
|
||||
expect(reset.sessionInputs[0]?.resumeSessionId).toBeUndefined();
|
||||
for (const { meta } of [fresh, resumed, reset]) {
|
||||
const prompt = String(meta[0]?.prompt ?? "");
|
||||
expect(prompt).toContain(chatDirective);
|
||||
expect(prompt).not.toContain("Execution contract:");
|
||||
expect(prompt).not.toContain("clear final disposition");
|
||||
expect(prompt).not.toContain("Create child issues");
|
||||
expect(prompt).not.toContain("Use child issues");
|
||||
}
|
||||
expect(String(fresh.meta[0]?.prompt)).toContain(custom ? "Custom agent instructions." : "Continue your Paperclip conversation");
|
||||
expect(String(reset.meta[0]?.prompt)).toContain(custom ? "Custom agent instructions." : "Continue your Paperclip conversation");
|
||||
const ordinary = await runExecutor({ ...config, promptTemplate: "" }, { context: { ...context, conversationMode: false } });
|
||||
expect(String(ordinary.meta[0]?.prompt)).toContain("Execution contract:");
|
||||
expect(String(ordinary.meta[0]?.prompt)).toContain("Create child issues from the approved plan");
|
||||
});
|
||||
|
||||
it("uses only the guarded external-chat contract for a default ACPX prompt", async () => {
|
||||
const { meta } = await runExecutor(
|
||||
{ agent: "custom", agentCommand: "node ./fake-acp.js" },
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import {
|
|||
} from "../workspace-restore-merge.js";
|
||||
import {
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE,
|
||||
applyPaperclipWorkspaceEnv,
|
||||
asNumber,
|
||||
asString,
|
||||
|
|
@ -2923,7 +2924,9 @@ async function buildPrompt(ctx: AdapterExecutionContext, resumedSession: boolean
|
|||
const hasCustomPromptTemplate = configuredPromptTemplate.trim().length > 0;
|
||||
const promptTemplate = hasCustomPromptTemplate
|
||||
? configuredPromptTemplate
|
||||
: DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE;
|
||||
: context.conversationMode === true
|
||||
? DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE
|
||||
: DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE;
|
||||
const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
|
||||
const instructionsDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : "";
|
||||
let instructionsPrefix = "";
|
||||
|
|
@ -2967,6 +2970,7 @@ async function buildPrompt(ctx: AdapterExecutionContext, resumedSession: boolean
|
|||
const externalChatTurn = isPaperclipExternalChatTurn(context.paperclipWake);
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, {
|
||||
resumedSession,
|
||||
conversationMode: context.conversationMode === true,
|
||||
// The task-context markdown is the authoritative brief on this lane; keep
|
||||
// the wake prompt's description copy out so the prompt carries it once.
|
||||
suppressIssueDescription: taskContextNote.length > 0,
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ function createMockSdkAgent(options: MockAgentOptions = {}) {
|
|||
const sendRun = options.sendRun ?? createMockRun();
|
||||
return {
|
||||
agentId: options.agentId ?? sendRun.agentId,
|
||||
send: vi.fn(async () => sendRun),
|
||||
send: vi.fn(async (_prompt: string, _options?: Record<string, unknown>) => sendRun),
|
||||
[Symbol.asyncDispose]: vi.fn(async () => {}),
|
||||
};
|
||||
}
|
||||
|
|
@ -142,6 +142,32 @@ describe("cursor_cloud execute", () => {
|
|||
getRunMock.mockReset();
|
||||
});
|
||||
|
||||
it.each([false, true])("sends the central chat directive to Cursor Cloud (custom=%s)", async (custom) => {
|
||||
const sdkAgent = createMockSdkAgent();
|
||||
createMock.mockResolvedValue(sdkAgent);
|
||||
const ctx = createContext();
|
||||
if (!custom) delete ctx.config.promptTemplate;
|
||||
const directive = "Chat directive: clarify goals and hand plans off to project tasks.";
|
||||
ctx.context = {
|
||||
...ctx.context,
|
||||
conversationMode: true,
|
||||
paperclipTaskMarkdown: directive,
|
||||
paperclipWake: {
|
||||
reason: "issue_commented",
|
||||
issue: { id: "issue-1", workMode: "planning", status: "in_progress" },
|
||||
interactionKind: "request_confirmation",
|
||||
interactionStatus: "accepted",
|
||||
},
|
||||
};
|
||||
const result = await execute(ctx);
|
||||
expect(result.exitCode).toBe(0);
|
||||
const prompt = String(sdkAgent.send.mock.calls[0]?.[0]);
|
||||
expect(prompt).toContain(directive);
|
||||
expect(prompt).toContain(custom ? "Do the work for" : "Continue your Paperclip conversation");
|
||||
expect(prompt).not.toContain("Execution contract:");
|
||||
expect(prompt).not.toContain("Create child issues");
|
||||
});
|
||||
|
||||
it("creates a fresh Cursor agent and injects Paperclip env without CURSOR_API_KEY", async () => {
|
||||
const run = createMockRun({
|
||||
agentId: "agent-fresh",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
import type { AdapterExecutionContext, AdapterExecutionResult, AdapterInvocationMeta } from "@paperclipai/adapter-utils";
|
||||
import {
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE,
|
||||
asBoolean,
|
||||
asString,
|
||||
buildPaperclipEnv,
|
||||
|
|
@ -20,6 +21,7 @@ import {
|
|||
parseObject,
|
||||
readPaperclipIssueWorkModeFromContext,
|
||||
renderPaperclipWakePrompt,
|
||||
selectPaperclipTaskMarkdown,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
renderTemplate,
|
||||
stringifyPaperclipWakePayload,
|
||||
|
|
@ -400,7 +402,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
}
|
||||
: null);
|
||||
const canReuseSession = sessionMatches(session, envType, envName, repos);
|
||||
const promptTemplate = asString(config.promptTemplate, DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE);
|
||||
const promptTemplate = asString(config.promptTemplate, context.conversationMode === true
|
||||
? DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE
|
||||
: DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE);
|
||||
const bootstrapPromptTemplate = asString(config.bootstrapPromptTemplate, "");
|
||||
const templateData = {
|
||||
agentId: agent.id,
|
||||
|
|
@ -412,7 +416,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
context,
|
||||
};
|
||||
const instructions = await buildInstructionsPrefix(config, onLog);
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: canReuseSession });
|
||||
const taskContextNote = context.conversationMode === true
|
||||
? selectPaperclipTaskMarkdown(context, { resumedSession: canReuseSession })
|
||||
: "";
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, {
|
||||
conversationMode: context.conversationMode === true,
|
||||
resumedSession: canReuseSession,
|
||||
suppressIssueDescription: taskContextNote.length > 0,
|
||||
});
|
||||
const renderedBootstrapPrompt =
|
||||
!canReuseSession && bootstrapPromptTemplate.trim().length > 0
|
||||
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
||||
|
|
@ -426,6 +437,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
instructions.prefix,
|
||||
renderedBootstrapPrompt,
|
||||
wakePrompt,
|
||||
taskContextNote,
|
||||
paperclipEnvNote,
|
||||
renderedPrompt,
|
||||
]);
|
||||
|
|
@ -465,6 +477,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
instructionsChars: instructions.chars,
|
||||
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
||||
wakePromptChars: wakePrompt.length,
|
||||
taskContextChars: taskContextNote.length,
|
||||
heartbeatPromptChars: renderedPrompt.length,
|
||||
},
|
||||
context: {
|
||||
|
|
|
|||
|
|
@ -44,9 +44,11 @@ import {
|
|||
removeMaintainerOnlySkillSymlinks,
|
||||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
selectPaperclipTaskMarkdown,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE,
|
||||
joinPromptSections,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import { DEFAULT_CURSOR_LOCAL_MODEL, SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
|
|
@ -206,7 +208,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
|
||||
const promptTemplate = asString(
|
||||
config.promptTemplate,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
context.conversationMode === true
|
||||
? DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE
|
||||
: DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
);
|
||||
let command = asString(config.command, "agent");
|
||||
const model = asString(config.model, DEFAULT_CURSOR_LOCAL_MODEL).trim();
|
||||
|
|
@ -566,7 +570,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
!sessionId && bootstrapPromptTemplate.trim().length > 0
|
||||
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
||||
: "";
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) });
|
||||
const taskContextNote = context.conversationMode === true
|
||||
? selectPaperclipTaskMarkdown(context, { resumedSession: Boolean(sessionId) })
|
||||
: "";
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, {
|
||||
conversationMode: context.conversationMode === true,
|
||||
resumedSession: Boolean(sessionId),
|
||||
suppressIssueDescription: taskContextNote.length > 0,
|
||||
});
|
||||
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
|
||||
const renderedPrompt = shouldUseResumeDeltaPrompt || isPaperclipRecoveryWakePayload(context.paperclipWake)
|
||||
? ""
|
||||
|
|
@ -577,6 +588,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
instructionsPrefix,
|
||||
renderedBootstrapPrompt,
|
||||
wakePrompt,
|
||||
taskContextNote,
|
||||
sessionHandoffNote,
|
||||
paperclipEnvNote,
|
||||
renderedPrompt,
|
||||
|
|
@ -586,6 +598,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
instructionsChars,
|
||||
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
||||
wakePromptChars: wakePrompt.length,
|
||||
taskContextChars: taskContextNote.length,
|
||||
sessionHandoffChars: sessionHandoffNote.length,
|
||||
runtimeNoteChars: paperclipEnvNote.length,
|
||||
heartbeatPromptChars: renderedPrompt.length,
|
||||
|
|
|
|||
|
|
@ -47,9 +47,11 @@ import {
|
|||
parseObject,
|
||||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
selectPaperclipTaskMarkdown,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE,
|
||||
runChildProcess,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import { DEFAULT_GEMINI_LOCAL_MODEL, SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
|
|
@ -230,7 +232,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
|
||||
const promptTemplate = asString(
|
||||
config.promptTemplate,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
context.conversationMode === true
|
||||
? DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE
|
||||
: DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
);
|
||||
const command = asString(config.command, "gemini");
|
||||
const model = asString(config.model, DEFAULT_GEMINI_LOCAL_MODEL).trim();
|
||||
|
|
@ -555,7 +559,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
!sessionId && bootstrapPromptTemplate.trim().length > 0
|
||||
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
||||
: "";
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) });
|
||||
const taskContextNote = context.conversationMode === true
|
||||
? selectPaperclipTaskMarkdown(context, { resumedSession: Boolean(sessionId) })
|
||||
: "";
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, {
|
||||
conversationMode: context.conversationMode === true,
|
||||
resumedSession: Boolean(sessionId),
|
||||
suppressIssueDescription: taskContextNote.length > 0,
|
||||
});
|
||||
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
|
||||
const renderedPrompt = shouldUseResumeDeltaPrompt || isPaperclipRecoveryWakePayload(context.paperclipWake)
|
||||
? ""
|
||||
|
|
@ -567,6 +578,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
instructionsPrefix,
|
||||
renderedBootstrapPrompt,
|
||||
wakePrompt,
|
||||
taskContextNote,
|
||||
sessionHandoffNote,
|
||||
paperclipEnvNote,
|
||||
apiAccessNote,
|
||||
|
|
@ -577,6 +589,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
instructionsChars: instructionsPrefix.length,
|
||||
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
||||
wakePromptChars: wakePrompt.length,
|
||||
taskContextChars: taskContextNote.length,
|
||||
sessionHandoffChars: sessionHandoffNote.length,
|
||||
runtimeNoteChars: paperclipEnvNote.length + apiAccessNote.length,
|
||||
heartbeatPromptChars: renderedPrompt.length,
|
||||
|
|
|
|||
|
|
@ -34,11 +34,13 @@ import {
|
|||
readPaperclipRuntimeSkillEntries,
|
||||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
selectPaperclipTaskMarkdown,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
resolveLegacyPaperclipDesiredSkillNames,
|
||||
stringifyPaperclipWakePayload,
|
||||
refreshPaperclipWorkspaceEnvForExecution,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import { DEFAULT_GROK_LOCAL_MODEL } from "../index.js";
|
||||
import { copyBackGrokAuth } from "./grok-auth-copyback.js";
|
||||
|
|
@ -202,7 +204,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
|
||||
const promptTemplate = asString(
|
||||
config.promptTemplate,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
context.conversationMode === true
|
||||
? DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE
|
||||
: DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
);
|
||||
const command = asString(config.command, "grok");
|
||||
const model = asString(config.model, DEFAULT_GROK_LOCAL_MODEL).trim();
|
||||
|
|
@ -474,7 +478,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
run: { id: runId, source: "on_demand" },
|
||||
context,
|
||||
};
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) });
|
||||
const taskContextNote = context.conversationMode === true
|
||||
? selectPaperclipTaskMarkdown(context, { resumedSession: Boolean(sessionId) })
|
||||
: "";
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, {
|
||||
conversationMode: context.conversationMode === true,
|
||||
resumedSession: Boolean(sessionId),
|
||||
suppressIssueDescription: taskContextNote.length > 0,
|
||||
});
|
||||
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
|
||||
const renderedPrompt = shouldUseResumeDeltaPrompt || isPaperclipRecoveryWakePayload(context.paperclipWake)
|
||||
? ""
|
||||
|
|
@ -484,6 +495,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
const apiAccessNote = renderApiAccessNote(env);
|
||||
const prompt = joinPromptSections([
|
||||
wakePrompt,
|
||||
taskContextNote,
|
||||
sessionHandoffNote,
|
||||
paperclipEnvNote,
|
||||
apiAccessNote,
|
||||
|
|
@ -492,6 +504,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
const promptMetrics = {
|
||||
promptChars: prompt.length,
|
||||
wakePromptChars: wakePrompt.length,
|
||||
taskContextChars: taskContextNote.length,
|
||||
sessionHandoffChars: sessionHandoffNote.length,
|
||||
runtimeNoteChars: paperclipEnvNote.length + apiAccessNote.length,
|
||||
heartbeatPromptChars: renderedPrompt.length,
|
||||
|
|
|
|||
|
|
@ -174,6 +174,40 @@ describe("execute", () => {
|
|||
expect(body.session_id).toBe("paperclip:company:company-1:agent:agent-1:issue:issue-1");
|
||||
});
|
||||
|
||||
it.each([false, true])("preserves chat handoff policy on gateway turns (resumed=%s)", async (resumed) => {
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL) => new Response(JSON.stringify(
|
||||
String(input).endsWith("/v1/runs")
|
||||
? { run_id: "run-hermes-1", status: "started" }
|
||||
: { status: "completed", output: "done" },
|
||||
), { status: 200 }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const ctx = makeCtx({ apiBaseUrl: "http://127.0.0.1:8642", apiKey: "secret-key", timeoutSec: 5 });
|
||||
ctx.config.payloadTemplate = { input: "Custom gateway instruction." };
|
||||
const directive = "Chat directive: clarify goals and hand plans off to project tasks.";
|
||||
ctx.context = {
|
||||
conversationMode: true,
|
||||
issueId: "issue-1",
|
||||
paperclipTaskMarkdown: directive,
|
||||
paperclipTaskMarkdownCompact: directive,
|
||||
paperclipWake: {
|
||||
reason: "issue_commented",
|
||||
issue: { id: "issue-1", workMode: "planning", status: "in_progress" },
|
||||
interactionKind: "request_confirmation",
|
||||
interactionStatus: "accepted",
|
||||
},
|
||||
};
|
||||
if (resumed) ctx.runtime.sessionId = "prior-session";
|
||||
await execute(ctx);
|
||||
const calls = fetchMock.mock.calls as Array<[RequestInfo | URL, RequestInit?]>;
|
||||
const call = calls.find(([input]) => String(input).endsWith("/v1/runs"));
|
||||
const prompt = JSON.parse(String(call?.[1]?.body)).input as string;
|
||||
expect(prompt).toContain("Custom gateway instruction.");
|
||||
expect(prompt).toContain(directive);
|
||||
expect(prompt).not.toContain("Execution contract:");
|
||||
expect(prompt).not.toContain("clear final disposition");
|
||||
expect(prompt).not.toContain("Create child issues");
|
||||
});
|
||||
|
||||
it("sends the task brief once on fresh runs and compacts it on stable-session resumes", async () => {
|
||||
const description = "Update launch-card.svg and change the CTA to Try Team free.";
|
||||
const fullTaskMarkdown = [
|
||||
|
|
|
|||
|
|
@ -274,6 +274,7 @@ function buildInput(ctx: AdapterExecutionContext, paperclipApiUrl: string | null
|
|||
Boolean(nonEmpty(ctx.runtime?.sessionId));
|
||||
const taskMarkdown = nonEmpty(selectPaperclipTaskMarkdown(ctx.context, { resumedSession }));
|
||||
const wakePrompt = renderPaperclipWakePrompt(ctx.context.paperclipWake, {
|
||||
conversationMode: ctx.context.conversationMode === true,
|
||||
// The task-context markdown is the authoritative brief on this lane; keep
|
||||
// the wake prompt's description copy out so the prompt carries it once.
|
||||
suppressIssueDescription: Boolean(taskMarkdown),
|
||||
|
|
@ -293,7 +294,7 @@ function buildInput(ctx: AdapterExecutionContext, paperclipApiUrl: string | null
|
|||
...(paperclipApiUrl ? [`- Paperclip API URL: ${paperclipApiUrl}`] : []),
|
||||
...(issueWorkMode ? [`- Issue work mode: ${issueWorkMode}`] : []),
|
||||
"",
|
||||
...(isPaperclipRecoveryWakePayload(ctx.context.paperclipWake)
|
||||
...(ctx.context.conversationMode === true || isPaperclipRecoveryWakePayload(ctx.context.paperclipWake)
|
||||
? []
|
||||
: [
|
||||
"Execution contract:",
|
||||
|
|
@ -322,7 +323,10 @@ function buildInput(ctx: AdapterExecutionContext, paperclipApiUrl: string | null
|
|||
function buildRunBody(ctx: AdapterExecutionContext, sessionKey: string | null): Record<string, unknown> {
|
||||
const paperclipApiUrl = nonEmpty(ctx.config.paperclipApiUrl);
|
||||
const payloadTemplate = parseObject(ctx.config.payloadTemplate);
|
||||
const input = nonEmpty(payloadTemplate.input) ?? buildInput(ctx, paperclipApiUrl);
|
||||
const configuredInput = nonEmpty(payloadTemplate.input);
|
||||
const input = configuredInput && ctx.context.conversationMode === true
|
||||
? `${configuredInput}\n\n${buildInput(ctx, paperclipApiUrl)}`
|
||||
: configuredInput ?? buildInput(ctx, paperclipApiUrl);
|
||||
const instructions =
|
||||
nonEmpty(ctx.config.instructions) ??
|
||||
nonEmpty(payloadTemplate.instructions) ??
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import {
|
|||
renderTemplate,
|
||||
ensureAbsoluteDirectory,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE,
|
||||
joinPromptSections,
|
||||
renderPaperclipWakePrompt,
|
||||
selectPaperclipTaskMarkdown,
|
||||
|
|
@ -140,9 +141,10 @@ export function buildPrompt(
|
|||
config: Record<string, unknown>,
|
||||
options: { resumedSession?: boolean } = {},
|
||||
): string {
|
||||
const template = cfgString(config.promptTemplate) || HERMES_DEFAULT_PROMPT_TEMPLATE;
|
||||
|
||||
const context = (ctx as any).context || {};
|
||||
const template = cfgString(config.promptTemplate) || (context.conversationMode === true
|
||||
? DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE
|
||||
: HERMES_DEFAULT_PROMPT_TEMPLATE);
|
||||
const taskId = cfgString(context.taskId) || cfgString(context.issueId) || cfgString(ctx.config?.taskId);
|
||||
const taskTitle = cfgString(context.taskTitle) || cfgString(ctx.config?.taskTitle) || "";
|
||||
const taskBody = cfgString(context.taskBody) || cfgString(ctx.config?.taskBody) || "";
|
||||
|
|
@ -166,6 +168,7 @@ export function buildPrompt(
|
|||
resumedSession: options.resumedSession === true,
|
||||
});
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, {
|
||||
conversationMode: context.conversationMode === true,
|
||||
resumedSession: options.resumedSession === true,
|
||||
// The task-context markdown is the authoritative brief on this lane; keep
|
||||
// the wake prompt's description copy out so the prompt carries it once.
|
||||
|
|
|
|||
|
|
@ -246,3 +246,23 @@ test("preserves custom prompt templates while exposing runtime and wake variable
|
|||
expect(prompt).toContain("Issue description:\n```text\nUse the wake payload as runtime authority.\n```");
|
||||
expect(prompt).not.toContain("Paperclip runtime identity:");
|
||||
});
|
||||
|
||||
|
||||
test.each([false, true])("conversation prompts preserve the handoff policy (resumed=%s)", (resumedSession) => {
|
||||
const directive = "Chat directive: clarify goals and hand the plan off to project tasks.";
|
||||
const ctx = baseContext({
|
||||
conversationMode: true,
|
||||
paperclipTaskMarkdown: directive,
|
||||
paperclipTaskMarkdownCompact: directive,
|
||||
});
|
||||
ctx.context.paperclipWake.interactionKind = "request_confirmation";
|
||||
ctx.context.paperclipWake.interactionStatus = "accepted";
|
||||
for (const config of [{}, { promptTemplate: "Custom agent instruction." }]) {
|
||||
const prompt = buildPrompt(ctx, config, { resumedSession });
|
||||
expect(prompt).toContain(directive);
|
||||
expect(prompt).not.toContain("Execution contract:");
|
||||
expect(prompt).not.toContain("clear final disposition");
|
||||
expect(prompt).not.toContain("Create child issues");
|
||||
expect(prompt).not.toContain("--arg status done");
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -39,9 +39,11 @@ import {
|
|||
parseObject,
|
||||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
selectPaperclipTaskMarkdown,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import {
|
||||
SANDBOX_INSTALL_COMMAND,
|
||||
|
|
@ -210,7 +212,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
|
||||
const promptTemplate = asString(
|
||||
config.promptTemplate,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
context.conversationMode === true
|
||||
? DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE
|
||||
: DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
);
|
||||
const command = asString(config.command, "kimi");
|
||||
const model = asString(config.model, "").trim();
|
||||
|
|
@ -512,7 +516,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
!sessionId && bootstrapPromptTemplate.trim().length > 0
|
||||
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
||||
: "";
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) });
|
||||
const taskContextNote = context.conversationMode === true
|
||||
? selectPaperclipTaskMarkdown(context, { resumedSession: Boolean(sessionId) })
|
||||
: "";
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, {
|
||||
conversationMode: context.conversationMode === true,
|
||||
resumedSession: Boolean(sessionId),
|
||||
suppressIssueDescription: taskContextNote.length > 0,
|
||||
});
|
||||
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
|
||||
const renderedPrompt = shouldUseResumeDeltaPrompt || isPaperclipRecoveryWakePayload(context.paperclipWake)
|
||||
? ""
|
||||
|
|
@ -524,6 +535,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
instructionsPrefix,
|
||||
renderedBootstrapPrompt,
|
||||
wakePrompt,
|
||||
taskContextNote,
|
||||
sessionHandoffNote,
|
||||
paperclipEnvNote,
|
||||
apiAccessNote,
|
||||
|
|
@ -534,6 +546,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
instructionsChars: instructionsPrefix.length,
|
||||
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
||||
wakePromptChars: wakePrompt.length,
|
||||
taskContextChars: taskContextNote.length,
|
||||
sessionHandoffChars: sessionHandoffNote.length,
|
||||
runtimeNoteChars: paperclipEnvNote.length + apiAccessNote.length,
|
||||
heartbeatPromptChars: renderedPrompt.length,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const websocketState = vi.hoisted(() => ({
|
|||
failConnectAttempts: 0,
|
||||
failAgentRequests: 0,
|
||||
events: [] as string[],
|
||||
messages: [] as string[],
|
||||
}));
|
||||
|
||||
vi.mock("ws", async () => {
|
||||
|
|
@ -35,7 +36,8 @@ vi.mock("ws", async () => {
|
|||
}
|
||||
|
||||
send(payload: string) {
|
||||
const request = JSON.parse(payload) as { id: string; method: string };
|
||||
const request = JSON.parse(payload) as { id: string; method: string; params?: { message?: string } };
|
||||
if (request.method === "agent") websocketState.messages.push(request.params?.message ?? "");
|
||||
websocketState.events.push(`send:${request.method}`);
|
||||
if (request.method === "agent" && websocketState.failAgentRequests > 0) {
|
||||
websocketState.failAgentRequests--;
|
||||
|
|
@ -105,12 +107,41 @@ describe("openclaw_gateway execute dispatch boundary", () => {
|
|||
websocketState.failConnectAttempts = 0;
|
||||
websocketState.failAgentRequests = 0;
|
||||
websocketState.events = [];
|
||||
websocketState.messages = [];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it.each([false, true])("sends conversation policy without the issue-completion workflow (resumed=%s)", async (resumed) => {
|
||||
const ctx = createContext();
|
||||
const directive = "Chat directive: clarify goals and hand plans off to project tasks.";
|
||||
ctx.context = {
|
||||
...ctx.context,
|
||||
conversationMode: true,
|
||||
paperclipTaskMarkdown: directive,
|
||||
paperclipTaskMarkdownCompact: directive,
|
||||
paperclipWake: {
|
||||
reason: "issue_commented",
|
||||
issue: { id: "issue-1", workMode: "planning", status: "in_progress" },
|
||||
interactionKind: "request_confirmation",
|
||||
interactionStatus: "accepted",
|
||||
},
|
||||
};
|
||||
if (resumed) ctx.runtime.sessionId = "prior-session";
|
||||
const result = await execute(ctx);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(websocketState.messages).toHaveLength(1);
|
||||
const prompt = websocketState.messages[0]!;
|
||||
expect(prompt).toContain(directive);
|
||||
expect(prompt).toContain("X-Paperclip-Run-Id");
|
||||
expect(prompt).not.toContain("Execution contract:");
|
||||
expect(prompt).not.toContain("Create child issues");
|
||||
expect(prompt).not.toContain('"status":"done"');
|
||||
expect(prompt).not.toContain("GET /api/issues/{issueId}/comments");
|
||||
});
|
||||
|
||||
it("reports dispatch after transport setup and before the remote agent request", async () => {
|
||||
const onDispatch = vi.fn(() => {
|
||||
websocketState.events.push("dispatch");
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
parseObject,
|
||||
readPaperclipIssueWorkModeFromContext,
|
||||
renderPaperclipWakePrompt,
|
||||
selectPaperclipTaskMarkdown,
|
||||
stringifyPaperclipWakePayload,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import crypto, { randomUUID } from "node:crypto";
|
||||
|
|
@ -372,6 +373,7 @@ function buildWakeText(
|
|||
paperclipEnv: Record<string, string>,
|
||||
structuredWakePrompt: string,
|
||||
claimedApiKeyPath: string,
|
||||
conversationTaskMarkdown?: string,
|
||||
): string {
|
||||
const orderedKeys = [
|
||||
"PAPERCLIP_RUN_ID",
|
||||
|
|
@ -396,6 +398,19 @@ function buildWakeText(
|
|||
const issueIdHint = payload.taskId ?? payload.issueId ?? "";
|
||||
const apiBaseHint = paperclipEnv.PAPERCLIP_API_URL ?? "<set PAPERCLIP_API_URL>";
|
||||
|
||||
if (conversationTaskMarkdown !== undefined) {
|
||||
return [
|
||||
"Paperclip conversation turn for a cloud adapter.",
|
||||
"Set these values in your run context:",
|
||||
...envLines,
|
||||
`Load PAPERCLIP_API_KEY from ${claimedApiKeyPath} (the token saved after claim-api-key).`,
|
||||
"Use Authorization: Bearer $PAPERCLIP_API_KEY on every API call and X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID on every mutation.",
|
||||
"Follow the supplied chat mode directive. Keep this conversation available for the next message.",
|
||||
structuredWakePrompt,
|
||||
conversationTaskMarkdown,
|
||||
].join("\n\n");
|
||||
}
|
||||
|
||||
const lines = [
|
||||
"Paperclip wake event for a cloud adapter.",
|
||||
"",
|
||||
|
|
@ -1091,6 +1106,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
// must carry the execution contract itself.
|
||||
const structuredWakePrompt = renderPaperclipWakePrompt(ctx.context.paperclipWake, {
|
||||
includeExecutionContract: true,
|
||||
conversationMode: ctx.context.conversationMode === true,
|
||||
});
|
||||
const structuredWakeJson = stringifyPaperclipWakePayload(ctx.context.paperclipWake);
|
||||
const wakeText = buildWakeText(
|
||||
|
|
@ -1100,6 +1116,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
? joinWakePayloadSections(structuredWakePrompt, structuredWakeJson)
|
||||
: structuredWakePrompt,
|
||||
resolveClaimedApiKeyPath(ctx.config.claimedApiKeyPath),
|
||||
ctx.context.conversationMode === true
|
||||
? selectPaperclipTaskMarkdown(ctx.context, { resumedSession: Boolean(ctx.runtime?.sessionId) })
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const sessionKeyStrategy = normalizeSessionKeyStrategy(ctx.config.sessionKeyStrategy);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,41 @@ describe("OpenCode local skill injection", () => {
|
|||
await fs.rm(configHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it.each([false, true])("keeps chat policy with a legacy OpenCode prompt (custom=%s)", async (custom) => {
|
||||
const commandPath = path.join(configHome, "fake-opencode");
|
||||
await fs.writeFile(commandPath, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
||||
runProcessMock.mockReset();
|
||||
runProcessMock.mockResolvedValue(probeResult({ stdout: JSON.stringify({
|
||||
type: "text", sessionID: "chat-session", part: { text: "Reply" },
|
||||
}) }));
|
||||
const directive = "Chat directive: clarify goals and hand plans off to project tasks.";
|
||||
let prompt = "";
|
||||
const result = await execute({
|
||||
runId: "chat-run",
|
||||
agent: { id: "agent-1", companyId: "company-1", name: "OpenCode", adapterType: "opencode_local", adapterConfig: {} },
|
||||
runtime: { sessionId: null, sessionParams: null, sessionDisplayId: null, taskKey: null },
|
||||
config: {
|
||||
command: commandPath, cwd: configHome, model: "openai/gpt-5", env: { OPENCODE_ALLOW_ALL_MODELS: "1" },
|
||||
...(custom ? { promptTemplate: "Custom agent instruction." } : {}),
|
||||
},
|
||||
context: {
|
||||
conversationMode: true,
|
||||
paperclipTaskMarkdown: directive,
|
||||
paperclipWake: {
|
||||
reason: "issue_commented", issue: { id: "chat-1", status: "in_progress", workMode: "planning" },
|
||||
interactionKind: "request_confirmation", interactionStatus: "accepted",
|
||||
},
|
||||
},
|
||||
onLog: async () => {},
|
||||
onMeta: async (meta) => { prompt = String(meta.prompt ?? ""); },
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(prompt).toContain(directive);
|
||||
expect(prompt).toContain(custom ? "Custom agent instruction." : "Continue your Paperclip conversation");
|
||||
expect(prompt).not.toContain("Execution contract:");
|
||||
expect(prompt).not.toContain("Create child issues");
|
||||
});
|
||||
|
||||
it("injects runtime skills into the configured child HOME", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-opencode-configured-home-"));
|
||||
const processHome = path.join(root, "process-home");
|
||||
|
|
|
|||
|
|
@ -40,9 +40,11 @@ import {
|
|||
refreshPaperclipWorkspaceEnvForExecution,
|
||||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
selectPaperclipTaskMarkdown,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE,
|
||||
runChildProcess,
|
||||
isPaperclipSkillSourceMissing,
|
||||
readPaperclipRuntimeSkillEntries,
|
||||
|
|
@ -229,7 +231,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
|
||||
const promptTemplate = asString(
|
||||
config.promptTemplate,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
context.conversationMode === true
|
||||
? DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE
|
||||
: DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
);
|
||||
const command = asString(config.command, "opencode");
|
||||
const model = asString(config.model, "").trim();
|
||||
|
|
@ -560,7 +564,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
!sessionId && bootstrapPromptTemplate.trim().length > 0
|
||||
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
||||
: "";
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) });
|
||||
const taskContextNote = context.conversationMode === true
|
||||
? selectPaperclipTaskMarkdown(context, { resumedSession: Boolean(sessionId) })
|
||||
: "";
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, {
|
||||
conversationMode: context.conversationMode === true,
|
||||
resumedSession: Boolean(sessionId),
|
||||
suppressIssueDescription: taskContextNote.length > 0,
|
||||
});
|
||||
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
|
||||
const renderedPrompt = shouldUseResumeDeltaPrompt || isPaperclipRecoveryWakePayload(context.paperclipWake)
|
||||
? ""
|
||||
|
|
@ -570,6 +581,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
instructionsPrefix,
|
||||
renderedBootstrapPrompt,
|
||||
wakePrompt,
|
||||
taskContextNote,
|
||||
sessionHandoffNote,
|
||||
renderedPrompt,
|
||||
]);
|
||||
|
|
@ -578,6 +590,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
instructionsChars: instructionsPrefix.length,
|
||||
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
||||
wakePromptChars: wakePrompt.length,
|
||||
taskContextChars: taskContextNote.length,
|
||||
sessionHandoffChars: sessionHandoffNote.length,
|
||||
heartbeatPromptChars: renderedPrompt.length,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -45,9 +45,11 @@ import {
|
|||
removeMaintainerOnlySkillSymlinks,
|
||||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
selectPaperclipTaskMarkdown,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE,
|
||||
runChildProcess,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import { shellQuote } from "@paperclipai/adapter-utils/ssh";
|
||||
|
|
@ -228,7 +230,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
|
||||
const promptTemplate = asString(
|
||||
config.promptTemplate,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
context.conversationMode === true
|
||||
? DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE
|
||||
: DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
);
|
||||
const command = asString(config.command, "pi");
|
||||
const model = asString(config.model, "").trim();
|
||||
|
|
@ -583,7 +587,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
`${instructionsContents}\n\n` +
|
||||
`The above agent instructions were loaded from ${resolvedInstructionsFilePath}. ` +
|
||||
`Resolve any relative file references from ${instructionsFileDir}.\n\n` +
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE;
|
||||
(context.conversationMode === true
|
||||
? DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE
|
||||
: DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE);
|
||||
} catch (err) {
|
||||
instructionsReadFailed = true;
|
||||
const reason = err instanceof Error ? err.message : String(err);
|
||||
|
|
@ -613,7 +619,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
!canResumeSession && bootstrapPromptTemplate.trim().length > 0
|
||||
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
||||
: "";
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: canResumeSession });
|
||||
const taskContextNote = context.conversationMode === true
|
||||
? selectPaperclipTaskMarkdown(context, { resumedSession: canResumeSession })
|
||||
: "";
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, {
|
||||
conversationMode: context.conversationMode === true,
|
||||
resumedSession: canResumeSession,
|
||||
suppressIssueDescription: taskContextNote.length > 0,
|
||||
});
|
||||
const shouldUseResumeDeltaPrompt = canResumeSession && wakePrompt.length > 0;
|
||||
const renderedHeartbeatPrompt = shouldUseResumeDeltaPrompt || isPaperclipRecoveryWakePayload(context.paperclipWake)
|
||||
? ""
|
||||
|
|
@ -622,6 +635,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
const userPrompt = joinPromptSections([
|
||||
renderedBootstrapPrompt,
|
||||
wakePrompt,
|
||||
taskContextNote,
|
||||
sessionHandoffNote,
|
||||
renderedHeartbeatPrompt,
|
||||
]);
|
||||
|
|
@ -630,6 +644,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
promptChars: userPrompt.length,
|
||||
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
||||
wakePromptChars: wakePrompt.length,
|
||||
taskContextChars: taskContextNote.length,
|
||||
sessionHandoffChars: sessionHandoffNote.length,
|
||||
heartbeatPromptChars: renderedHeartbeatPrompt.length,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue