mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
[codex] Add run liveness continuations (#4083)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies. > - Heartbeat runs are the control-plane record of each agent execution window. > - Long-running local agents can exhaust context or stop while still holding useful next-step state. > - Operators need that stop reason, next action, and continuation path to be durable and visible. > - This pull request adds run liveness metadata, continuation summaries, and UI surfaces for issue run ledgers. > - The benefit is that interrupted or long-running work can resume with clearer context instead of losing the agent's last useful handoff. ## What Changed - Added heartbeat-run liveness fields, continuation attempt tracking, and an idempotent `0058` migration. - Added server services and tests for run liveness, continuation summaries, stop metadata, and activity backfill. - Wired local and HTTP adapters to surface continuation/liveness context through shared adapter utilities. - Added shared constants, validators, and heartbeat types for liveness continuation state. - Added issue-detail UI surfaces for continuation handoffs and the run ledger, with component tests. - Updated agent runtime docs, heartbeat protocol docs, prompt guidance, onboarding assets, and skills instructions to explain continuation behavior. - Addressed Greptile feedback by scoping document evidence by run, excluding system continuation-summary documents from liveness evidence, importing shared liveness types, surfacing hidden ledger run counts, documenting bounded retry behavior, and moving run-ledger liveness backfill off the request path. ## Verification - `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts server/src/__tests__/run-continuations.test.ts server/src/__tests__/run-liveness.test.ts server/src/__tests__/activity-service.test.ts server/src/__tests__/documents-service.test.ts server/src/__tests__/issue-continuation-summary.test.ts server/src/services/heartbeat-stop-metadata.test.ts ui/src/components/IssueRunLedger.test.tsx ui/src/components/IssueContinuationHandoff.test.tsx ui/src/components/IssueDocumentsSection.test.tsx` - `pnpm --filter @paperclipai/db build` - `pnpm exec vitest run server/src/__tests__/activity-service.test.ts ui/src/components/IssueRunLedger.test.tsx` - `pnpm --filter @paperclipai/ui typecheck` - `pnpm --filter @paperclipai/server typecheck` - `pnpm exec vitest run server/src/__tests__/activity-service.test.ts server/src/__tests__/run-continuations.test.ts ui/src/components/IssueRunLedger.test.tsx` - `pnpm exec vitest run server/src/__tests__/heartbeat-process-recovery.test.ts -t "treats a plan document update"` - `pnpm exec vitest run server/src/__tests__/activity-service.test.ts server/src/__tests__/heartbeat-process-recovery.test.ts -t "activity service|treats a plan document update"` - Remote PR checks on head `e53b1a1d`: `verify`, `e2e`, `policy`, and Snyk all passed. - Confirmed `public-gh/master` is an ancestor of this branch after fetching `public-gh master`. - Confirmed `pnpm-lock.yaml` is not included in the branch diff. - Confirmed migration `0058_wealthy_starbolt.sql` is ordered after `0057` and uses `IF NOT EXISTS` guards for repeat application. - Greptile inline review threads are resolved. ## Risks - Medium risk: this touches heartbeat execution, liveness recovery, activity rendering, issue routes, shared contracts, docs, and UI. - Migration risk is mitigated by additive columns/indexes and idempotent guards. - Run-ledger liveness backfill is now asynchronous, so the first ledger response can briefly show historical missing liveness until the background backfill completes. - UI screenshot coverage is not included in this packaging pass; validation is currently through focused component tests. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, GPT-5.4, local tool-use coding agent with terminal, git, GitHub connector, GitHub CLI, and Paperclip API access. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge Screenshot note: no before/after screenshots were captured in this PR packaging pass; the UI changes are covered by focused component tests listed above. --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
b9a80dcf22
commit
236d11d36f
71 changed files with 18254 additions and 85 deletions
|
|
@ -1,6 +1,11 @@
|
|||
import { randomUUID } from "node:crypto";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { runChildProcess } from "./server-utils.js";
|
||||
import {
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
renderPaperclipWakePrompt,
|
||||
runChildProcess,
|
||||
stringifyPaperclipWakePayload,
|
||||
} from "./server-utils.js";
|
||||
|
||||
function isPidAlive(pid: number) {
|
||||
try {
|
||||
|
|
@ -21,6 +26,25 @@ async function waitForPidExit(pid: number, timeoutMs = 2_000) {
|
|||
}
|
||||
|
||||
describe("runChildProcess", () => {
|
||||
it("does not arm a timeout when timeoutSec is 0", async () => {
|
||||
const result = await runChildProcess(
|
||||
randomUUID(),
|
||||
process.execPath,
|
||||
["-e", "setTimeout(() => process.stdout.write('done'), 150);"],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: {},
|
||||
timeoutSec: 0,
|
||||
graceSec: 1,
|
||||
onLog: async () => {},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.timedOut).toBe(false);
|
||||
expect(result.stdout).toBe("done");
|
||||
});
|
||||
|
||||
it("waits for onSpawn before sending stdin to the child", async () => {
|
||||
const spawnDelayMs = 150;
|
||||
const startedAt = Date.now();
|
||||
|
|
@ -86,3 +110,108 @@ describe("runChildProcess", () => {
|
|||
expect(await waitForPidExit(descendantPid!, 2_000)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("renderPaperclipWakePrompt", () => {
|
||||
it("keeps the default local-agent prompt action-oriented", () => {
|
||||
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain("Start actionable work in this heartbeat");
|
||||
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain("do not stop at a plan");
|
||||
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain("Use child issues");
|
||||
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain("instead of polling agents, sessions, or processes");
|
||||
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain(
|
||||
"Respect budget, pause/cancel, approval gates, and company boundaries",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds the execution contract to scoped wake prompts", () => {
|
||||
const prompt = renderPaperclipWakePrompt({
|
||||
reason: "issue_assigned",
|
||||
issue: {
|
||||
id: "issue-1",
|
||||
identifier: "PAP-1580",
|
||||
title: "Update prompts",
|
||||
status: "in_progress",
|
||||
},
|
||||
commentWindow: {
|
||||
requestedCount: 0,
|
||||
includedCount: 0,
|
||||
missingCount: 0,
|
||||
},
|
||||
comments: [],
|
||||
fallbackFetchNeeded: false,
|
||||
});
|
||||
|
||||
expect(prompt).toContain("## Paperclip Wake Payload");
|
||||
expect(prompt).toContain("Execution contract: take concrete action in this heartbeat");
|
||||
expect(prompt).toContain("use child issues instead of polling");
|
||||
expect(prompt).toContain("mark blocked work with the unblock owner/action");
|
||||
});
|
||||
|
||||
it("includes continuation and child issue summaries in structured wake context", () => {
|
||||
const payload = {
|
||||
reason: "issue_children_completed",
|
||||
issue: {
|
||||
id: "parent-1",
|
||||
identifier: "PAP-100",
|
||||
title: "Integrate child work",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
},
|
||||
continuationSummary: {
|
||||
key: "continuation-summary",
|
||||
title: "Continuation Summary",
|
||||
body: "# Continuation Summary\n\n## Next Action\n\n- Integrate child outputs.",
|
||||
updatedAt: "2026-04-18T12:00:00.000Z",
|
||||
},
|
||||
livenessContinuation: {
|
||||
attempt: 2,
|
||||
maxAttempts: 2,
|
||||
sourceRunId: "run-1",
|
||||
state: "plan_only",
|
||||
reason: "Run described future work without concrete action evidence",
|
||||
instruction: "Take the first concrete action now.",
|
||||
},
|
||||
childIssueSummaries: [
|
||||
{
|
||||
id: "child-1",
|
||||
identifier: "PAP-101",
|
||||
title: "Implement helper",
|
||||
status: "done",
|
||||
priority: "medium",
|
||||
summary: "Added the helper route and tests.",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(JSON.parse(stringifyPaperclipWakePayload(payload) ?? "{}")).toMatchObject({
|
||||
continuationSummary: {
|
||||
body: expect.stringContaining("Continuation Summary"),
|
||||
},
|
||||
livenessContinuation: {
|
||||
attempt: 2,
|
||||
maxAttempts: 2,
|
||||
sourceRunId: "run-1",
|
||||
state: "plan_only",
|
||||
instruction: "Take the first concrete action now.",
|
||||
},
|
||||
childIssueSummaries: [
|
||||
{
|
||||
identifier: "PAP-101",
|
||||
summary: "Added the helper route and tests.",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const prompt = renderPaperclipWakePrompt(payload);
|
||||
expect(prompt).toContain("Issue continuation summary:");
|
||||
expect(prompt).toContain("Integrate child outputs.");
|
||||
expect(prompt).toContain("Run liveness continuation:");
|
||||
expect(prompt).toContain("- attempt: 2/2");
|
||||
expect(prompt).toContain("- source run: run-1");
|
||||
expect(prompt).toContain("- liveness state: plan_only");
|
||||
expect(prompt).toContain("- reason: Run described future work without concrete action evidence");
|
||||
expect(prompt).toContain("- instruction: Take the first concrete action now.");
|
||||
expect(prompt).toContain("Direct child issue summaries:");
|
||||
expect(prompt).toContain("PAP-101 Implement helper (done)");
|
||||
expect(prompt).toContain("Added the helper route and tests.");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -66,6 +66,17 @@ const PAPERCLIP_SKILL_ROOT_RELATIVE_CANDIDATES = [
|
|||
"../../../../../skills",
|
||||
];
|
||||
|
||||
export const DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE = [
|
||||
"You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work.",
|
||||
"",
|
||||
"Execution contract:",
|
||||
"- Start actionable work in this heartbeat; do not stop at a plan unless the issue asks for planning.",
|
||||
"- Leave durable progress in comments, documents, or work products with a clear next action.",
|
||||
"- Use child issues for parallel or long delegated work instead of polling agents, sessions, or processes.",
|
||||
"- If blocked, mark the issue blocked and name the unblock owner and action.",
|
||||
"- Respect budget, pause/cancel, approval gates, and company boundaries.",
|
||||
].join("\n");
|
||||
|
||||
export interface PaperclipSkillEntry {
|
||||
key: string;
|
||||
runtimeName: string;
|
||||
|
|
@ -250,11 +261,41 @@ type PaperclipWakeComment = {
|
|||
authorId: string | null;
|
||||
};
|
||||
|
||||
type PaperclipWakeContinuationSummary = {
|
||||
key: string | null;
|
||||
title: string | null;
|
||||
body: string;
|
||||
bodyTruncated: boolean;
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
||||
type PaperclipWakeLivenessContinuation = {
|
||||
attempt: number | null;
|
||||
maxAttempts: number | null;
|
||||
sourceRunId: string | null;
|
||||
state: string | null;
|
||||
reason: string | null;
|
||||
instruction: string | null;
|
||||
};
|
||||
|
||||
type PaperclipWakeChildIssueSummary = {
|
||||
id: string | null;
|
||||
identifier: string | null;
|
||||
title: string | null;
|
||||
status: string | null;
|
||||
priority: string | null;
|
||||
summary: string | null;
|
||||
};
|
||||
|
||||
type PaperclipWakePayload = {
|
||||
reason: string | null;
|
||||
issue: PaperclipWakeIssue | null;
|
||||
checkedOutByHarness: boolean;
|
||||
executionStage: PaperclipWakeExecutionStage | null;
|
||||
continuationSummary: PaperclipWakeContinuationSummary | null;
|
||||
livenessContinuation: PaperclipWakeLivenessContinuation | null;
|
||||
childIssueSummaries: PaperclipWakeChildIssueSummary[];
|
||||
childIssueSummaryTruncated: boolean;
|
||||
commentIds: string[];
|
||||
latestCommentId: string | null;
|
||||
comments: PaperclipWakeComment[];
|
||||
|
|
@ -298,6 +339,50 @@ function normalizePaperclipWakeComment(value: unknown): PaperclipWakeComment | n
|
|||
};
|
||||
}
|
||||
|
||||
function normalizePaperclipWakeContinuationSummary(value: unknown): PaperclipWakeContinuationSummary | null {
|
||||
const summary = parseObject(value);
|
||||
const body = asString(summary.body, "").trim();
|
||||
if (!body) return null;
|
||||
return {
|
||||
key: asString(summary.key, "").trim() || null,
|
||||
title: asString(summary.title, "").trim() || null,
|
||||
body,
|
||||
bodyTruncated: asBoolean(summary.bodyTruncated, false),
|
||||
updatedAt: asString(summary.updatedAt, "").trim() || null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePaperclipWakeLivenessContinuation(value: unknown): PaperclipWakeLivenessContinuation | null {
|
||||
const continuation = parseObject(value);
|
||||
const attempt = asNumber(continuation.attempt, 0);
|
||||
const maxAttempts = asNumber(continuation.maxAttempts, 0);
|
||||
const sourceRunId = asString(continuation.sourceRunId, "").trim() || null;
|
||||
const state = asString(continuation.state, "").trim() || null;
|
||||
const reason = asString(continuation.reason, "").trim() || null;
|
||||
const instruction = asString(continuation.instruction, "").trim() || null;
|
||||
if (!attempt && !maxAttempts && !sourceRunId && !state && !reason && !instruction) return null;
|
||||
return {
|
||||
attempt: attempt > 0 ? attempt : null,
|
||||
maxAttempts: maxAttempts > 0 ? maxAttempts : null,
|
||||
sourceRunId,
|
||||
state,
|
||||
reason,
|
||||
instruction,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePaperclipWakeChildIssueSummary(value: unknown): PaperclipWakeChildIssueSummary | null {
|
||||
const child = parseObject(value);
|
||||
const id = asString(child.id, "").trim() || null;
|
||||
const identifier = asString(child.identifier, "").trim() || null;
|
||||
const title = asString(child.title, "").trim() || null;
|
||||
const status = asString(child.status, "").trim() || null;
|
||||
const priority = asString(child.priority, "").trim() || null;
|
||||
const summary = asString(child.summary, "").trim() || null;
|
||||
if (!id && !identifier && !title && !status && !summary) return null;
|
||||
return { id, identifier, title, status, priority, summary };
|
||||
}
|
||||
|
||||
function normalizePaperclipWakeExecutionPrincipal(value: unknown): PaperclipWakeExecutionPrincipal | null {
|
||||
const principal = parseObject(value);
|
||||
const typeRaw = asString(principal.type, "").trim().toLowerCase();
|
||||
|
|
@ -356,8 +441,15 @@ export function normalizePaperclipWakePayload(value: unknown): PaperclipWakePayl
|
|||
.map((entry) => entry.trim())
|
||||
: [];
|
||||
const executionStage = normalizePaperclipWakeExecutionStage(payload.executionStage);
|
||||
const continuationSummary = normalizePaperclipWakeContinuationSummary(payload.continuationSummary);
|
||||
const livenessContinuation = normalizePaperclipWakeLivenessContinuation(payload.livenessContinuation);
|
||||
const childIssueSummaries = Array.isArray(payload.childIssueSummaries)
|
||||
? payload.childIssueSummaries
|
||||
.map((entry) => normalizePaperclipWakeChildIssueSummary(entry))
|
||||
.filter((entry): entry is PaperclipWakeChildIssueSummary => Boolean(entry))
|
||||
: [];
|
||||
|
||||
if (comments.length === 0 && commentIds.length === 0 && !executionStage && !normalizePaperclipWakeIssue(payload.issue)) {
|
||||
if (comments.length === 0 && commentIds.length === 0 && childIssueSummaries.length === 0 && !executionStage && !continuationSummary && !livenessContinuation && !normalizePaperclipWakeIssue(payload.issue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -366,6 +458,10 @@ export function normalizePaperclipWakePayload(value: unknown): PaperclipWakePayl
|
|||
issue: normalizePaperclipWakeIssue(payload.issue),
|
||||
checkedOutByHarness: asBoolean(payload.checkedOutByHarness, false),
|
||||
executionStage,
|
||||
continuationSummary,
|
||||
livenessContinuation,
|
||||
childIssueSummaries,
|
||||
childIssueSummaryTruncated: asBoolean(payload.childIssueSummaryTruncated, false),
|
||||
commentIds,
|
||||
latestCommentId: asString(payload.latestCommentId, "").trim() || null,
|
||||
comments,
|
||||
|
|
@ -406,6 +502,8 @@ export function renderPaperclipWakePrompt(
|
|||
"Focus on the new wake delta below and continue the current task without restating the full heartbeat boilerplate.",
|
||||
"Fetch the API thread only when `fallbackFetchNeeded` is true or you need broader history than this batch.",
|
||||
"",
|
||||
"Execution contract: take concrete action in this heartbeat when the issue is actionable; do not stop at a plan unless planning was requested. Leave durable progress with a clear next action, use child issues instead of polling for long or parallel work, and mark blocked work with the unblock owner/action.",
|
||||
"",
|
||||
`- reason: ${normalized.reason ?? "unknown"}`,
|
||||
`- issue: ${normalized.issue?.identifier ?? normalized.issue?.id ?? "unknown"}${normalized.issue?.title ? ` ${normalized.issue.title}` : ""}`,
|
||||
`- pending comments: ${normalized.includedCount}/${normalized.requestedCount}`,
|
||||
|
|
@ -421,6 +519,8 @@ export function renderPaperclipWakePrompt(
|
|||
"Use this inline wake data first before refetching the issue thread.",
|
||||
"Only fetch the API thread when `fallbackFetchNeeded` is true or you need broader history than this batch.",
|
||||
"",
|
||||
"Execution contract: take concrete action in this heartbeat when the issue is actionable; do not stop at a plan unless planning was requested. Leave durable progress with a clear next action, use child issues instead of polling for long or parallel work, and mark blocked work with the unblock owner/action.",
|
||||
"",
|
||||
`- reason: ${normalized.reason ?? "unknown"}`,
|
||||
`- issue: ${normalized.issue?.identifier ?? normalized.issue?.id ?? "unknown"}${normalized.issue?.title ? ` ${normalized.issue.title}` : ""}`,
|
||||
`- pending comments: ${normalized.includedCount}/${normalized.requestedCount}`,
|
||||
|
|
@ -470,6 +570,55 @@ export function renderPaperclipWakePrompt(
|
|||
}
|
||||
}
|
||||
|
||||
if (normalized.continuationSummary) {
|
||||
lines.push(
|
||||
"",
|
||||
"Issue continuation summary:",
|
||||
normalized.continuationSummary.body,
|
||||
);
|
||||
if (normalized.continuationSummary.bodyTruncated) {
|
||||
lines.push("[continuation summary truncated]");
|
||||
}
|
||||
}
|
||||
|
||||
if (normalized.livenessContinuation) {
|
||||
const continuation = normalized.livenessContinuation;
|
||||
lines.push("", "Run liveness continuation:");
|
||||
if (continuation.attempt) {
|
||||
lines.push(
|
||||
`- attempt: ${continuation.attempt}${continuation.maxAttempts ? `/${continuation.maxAttempts}` : ""}`,
|
||||
);
|
||||
}
|
||||
if (continuation.sourceRunId) {
|
||||
lines.push(`- source run: ${continuation.sourceRunId}`);
|
||||
}
|
||||
if (continuation.state) {
|
||||
lines.push(`- liveness state: ${continuation.state}`);
|
||||
}
|
||||
if (continuation.reason) {
|
||||
lines.push(`- reason: ${continuation.reason}`);
|
||||
}
|
||||
if (continuation.instruction) {
|
||||
lines.push(`- instruction: ${continuation.instruction}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (normalized.childIssueSummaries.length > 0) {
|
||||
lines.push("", "Direct child issue summaries:");
|
||||
for (const child of normalized.childIssueSummaries) {
|
||||
const label = child.identifier ?? child.id ?? "unknown";
|
||||
lines.push(
|
||||
`- ${label}${child.title ? ` ${child.title}` : ""}${child.status ? ` (${child.status})` : ""}`,
|
||||
);
|
||||
if (child.summary) {
|
||||
lines.push(` ${child.summary}`);
|
||||
}
|
||||
}
|
||||
if (normalized.childIssueSummaryTruncated) {
|
||||
lines.push("[child issue summaries truncated]");
|
||||
}
|
||||
}
|
||||
|
||||
if (normalized.checkedOutByHarness) {
|
||||
lines.push(
|
||||
"",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
runChildProcess,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import {
|
||||
|
|
@ -300,7 +301,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
|
||||
const promptTemplate = asString(
|
||||
config.promptTemplate,
|
||||
"You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work.",
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
);
|
||||
const model = asString(config.model, "");
|
||||
const effort = asString(config.effort, "");
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
joinPromptSections,
|
||||
runChildProcess,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
|
|
@ -218,7 +219,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
|
||||
const promptTemplate = asString(
|
||||
config.promptTemplate,
|
||||
"You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work.",
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
);
|
||||
const command = asString(config.command, "codex");
|
||||
const model = asString(config.model, "");
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
joinPromptSections,
|
||||
runChildProcess,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
|
|
@ -164,7 +165,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
|
||||
const promptTemplate = asString(
|
||||
config.promptTemplate,
|
||||
"You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work.",
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
);
|
||||
const command = asString(config.command, "agent");
|
||||
const model = asString(config.model, DEFAULT_CURSOR_LOCAL_MODEL).trim();
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
runChildProcess,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import { DEFAULT_GEMINI_LOCAL_MODEL } from "../index.js";
|
||||
|
|
@ -140,7 +141,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
|
||||
const promptTemplate = asString(
|
||||
config.promptTemplate,
|
||||
"You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work.",
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
);
|
||||
const command = asString(config.command, "gemini");
|
||||
const model = asString(config.model, DEFAULT_GEMINI_LOCAL_MODEL).trim();
|
||||
|
|
|
|||
|
|
@ -420,7 +420,9 @@ function buildWakeText(
|
|||
" - POST /api/issues/{issueId}/checkout with {\"agentId\":\"$PAPERCLIP_AGENT_ID\",\"expectedStatuses\":[\"todo\",\"backlog\",\"blocked\",\"in_review\"]}",
|
||||
" - GET /api/issues/{issueId}",
|
||||
" - GET /api/issues/{issueId}/comments",
|
||||
" - Execute the issue instructions exactly.",
|
||||
" - Execute the issue instructions exactly. If the issue is actionable, take concrete action in this run; do not stop at a plan unless planning was requested.",
|
||||
" - Leave durable progress with a clear next action. Use child issues for long or parallel delegated work instead of polling agents, sessions, or processes.",
|
||||
" - If blocked, PATCH /api/issues/{issueId} with {\"status\":\"blocked\",\"comment\":\"what is blocked, who owns the unblock, and the next action\"}.",
|
||||
" - If instructions require a comment, POST /api/issues/{issueId}/comments with {\"body\":\"...\"}.",
|
||||
" - PATCH /api/issues/{issueId} with {\"status\":\"done\",\"comment\":\"what changed and why\"}.",
|
||||
"4) If issueId does not exist:",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
runChildProcess,
|
||||
readPaperclipRuntimeSkillEntries,
|
||||
resolvePaperclipDesiredSkillNames,
|
||||
|
|
@ -97,7 +98,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
|
||||
const promptTemplate = asString(
|
||||
config.promptTemplate,
|
||||
"You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work.",
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
);
|
||||
const command = asString(config.command, "opencode");
|
||||
const model = asString(config.model, "").trim();
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
runChildProcess,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import { isPiUnknownSessionError, parsePiJsonl } from "./parse.js";
|
||||
|
|
@ -113,7 +114,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
|
||||
const promptTemplate = asString(
|
||||
config.promptTemplate,
|
||||
"You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work.",
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
);
|
||||
const command = asString(config.command, "pi");
|
||||
const model = asString(config.model, "").trim();
|
||||
|
|
@ -276,7 +277,7 @@ 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` +
|
||||
`You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work.`;
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE;
|
||||
} catch (err) {
|
||||
instructionsReadFailed = true;
|
||||
const reason = err instanceof Error ? err.message : String(err);
|
||||
|
|
|
|||
6
packages/db/src/migrations/0058_wealthy_starbolt.sql
Normal file
6
packages/db/src/migrations/0058_wealthy_starbolt.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "liveness_state" text;--> statement-breakpoint
|
||||
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "liveness_reason" text;--> statement-breakpoint
|
||||
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "continuation_attempt" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "last_useful_action_at" timestamp with time zone;--> statement-breakpoint
|
||||
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "next_action" text;--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "heartbeat_runs_company_liveness_idx" ON "heartbeat_runs" USING btree ("company_id","liveness_state","created_at");
|
||||
13490
packages/db/src/migrations/meta/0058_snapshot.json
Normal file
13490
packages/db/src/migrations/meta/0058_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -407,6 +407,13 @@
|
|||
"when": 1776309613598,
|
||||
"tag": "0057_tidy_join_requests",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 58,
|
||||
"version": "7",
|
||||
"when": 1776542245004,
|
||||
"tag": "0058_wealthy_starbolt",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -41,6 +41,11 @@ export const heartbeatRuns = pgTable(
|
|||
issueCommentStatus: text("issue_comment_status").notNull().default("not_applicable"),
|
||||
issueCommentSatisfiedByCommentId: uuid("issue_comment_satisfied_by_comment_id"),
|
||||
issueCommentRetryQueuedAt: timestamp("issue_comment_retry_queued_at", { withTimezone: true }),
|
||||
livenessState: text("liveness_state"),
|
||||
livenessReason: text("liveness_reason"),
|
||||
continuationAttempt: integer("continuation_attempt").notNull().default(0),
|
||||
lastUsefulActionAt: timestamp("last_useful_action_at", { withTimezone: true }),
|
||||
nextAction: text("next_action"),
|
||||
contextSnapshot: jsonb("context_snapshot").$type<Record<string, unknown>>(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
|
|
@ -51,5 +56,10 @@ export const heartbeatRuns = pgTable(
|
|||
table.agentId,
|
||||
table.startedAt,
|
||||
),
|
||||
companyLivenessIdx: index("heartbeat_runs_company_liveness_idx").on(
|
||||
table.companyId,
|
||||
table.livenessState,
|
||||
table.createdAt,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -141,6 +141,16 @@ export type IssueOriginKind = (typeof ISSUE_ORIGIN_KINDS)[number];
|
|||
export const ISSUE_RELATION_TYPES = ["blocks"] as const;
|
||||
export type IssueRelationType = (typeof ISSUE_RELATION_TYPES)[number];
|
||||
|
||||
export const ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY = "continuation-summary" as const;
|
||||
export const SYSTEM_ISSUE_DOCUMENT_KEYS = [ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY] as const;
|
||||
export type SystemIssueDocumentKey = (typeof SYSTEM_ISSUE_DOCUMENT_KEYS)[number];
|
||||
|
||||
const SYSTEM_ISSUE_DOCUMENT_KEY_SET = new Set<string>(SYSTEM_ISSUE_DOCUMENT_KEYS);
|
||||
|
||||
export function isSystemIssueDocumentKey(key: string): key is SystemIssueDocumentKey {
|
||||
return SYSTEM_ISSUE_DOCUMENT_KEY_SET.has(key);
|
||||
}
|
||||
|
||||
export const ISSUE_EXECUTION_POLICY_MODES = ["normal", "auto"] as const;
|
||||
export type IssueExecutionPolicyMode = (typeof ISSUE_EXECUTION_POLICY_MODES)[number];
|
||||
|
||||
|
|
@ -343,6 +353,17 @@ export const HEARTBEAT_RUN_STATUSES = [
|
|||
] as const;
|
||||
export type HeartbeatRunStatus = (typeof HEARTBEAT_RUN_STATUSES)[number];
|
||||
|
||||
export const RUN_LIVENESS_STATES = [
|
||||
"completed",
|
||||
"advanced",
|
||||
"plan_only",
|
||||
"empty_response",
|
||||
"blocked",
|
||||
"failed",
|
||||
"needs_followup",
|
||||
] as const;
|
||||
export type RunLivenessState = (typeof RUN_LIVENESS_STATES)[number];
|
||||
|
||||
export const LIVE_EVENT_TYPES = [
|
||||
"heartbeat.run.queued",
|
||||
"heartbeat.run.status",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ export {
|
|||
ISSUE_PRIORITIES,
|
||||
ISSUE_ORIGIN_KINDS,
|
||||
ISSUE_RELATION_TYPES,
|
||||
ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY,
|
||||
SYSTEM_ISSUE_DOCUMENT_KEYS,
|
||||
isSystemIssueDocumentKey,
|
||||
ISSUE_EXECUTION_POLICY_MODES,
|
||||
ISSUE_EXECUTION_STAGE_TYPES,
|
||||
ISSUE_EXECUTION_STATE_STATUSES,
|
||||
|
|
@ -49,6 +52,7 @@ export {
|
|||
BUDGET_INCIDENT_RESOLUTION_ACTIONS,
|
||||
HEARTBEAT_INVOCATION_SOURCES,
|
||||
HEARTBEAT_RUN_STATUSES,
|
||||
RUN_LIVENESS_STATES,
|
||||
WAKEUP_TRIGGER_DETAILS,
|
||||
WAKEUP_REQUEST_STATUSES,
|
||||
LIVE_EVENT_TYPES,
|
||||
|
|
@ -93,6 +97,7 @@ export {
|
|||
type IssuePriority,
|
||||
type IssueOriginKind,
|
||||
type IssueRelationType,
|
||||
type SystemIssueDocumentKey,
|
||||
type IssueExecutionPolicyMode,
|
||||
type IssueExecutionStageType,
|
||||
type IssueExecutionStateStatus,
|
||||
|
|
@ -125,6 +130,7 @@ export {
|
|||
type BudgetIncidentResolutionAction,
|
||||
type HeartbeatInvocationSource,
|
||||
type HeartbeatRunStatus,
|
||||
type RunLivenessState,
|
||||
type WakeupTriggerDetail,
|
||||
type WakeupRequestStatus,
|
||||
type LiveEventType,
|
||||
|
|
@ -494,6 +500,7 @@ export {
|
|||
type UpdateProjectWorkspace,
|
||||
projectExecutionWorkspacePolicySchema,
|
||||
createIssueSchema,
|
||||
createChildIssueSchema,
|
||||
createIssueLabelSchema,
|
||||
updateIssueSchema,
|
||||
issueExecutionPolicySchema,
|
||||
|
|
@ -521,6 +528,7 @@ export {
|
|||
upsertIssueDocumentSchema,
|
||||
restoreIssueDocumentRevisionSchema,
|
||||
type CreateIssue,
|
||||
type CreateChildIssue,
|
||||
type CreateIssueLabel,
|
||||
type UpdateIssue,
|
||||
type CheckoutIssue,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type {
|
|||
AgentStatus,
|
||||
HeartbeatInvocationSource,
|
||||
HeartbeatRunStatus,
|
||||
RunLivenessState,
|
||||
WakeupTriggerDetail,
|
||||
WakeupRequestStatus,
|
||||
} from "../constants.js";
|
||||
|
|
@ -38,6 +39,11 @@ export interface HeartbeatRun {
|
|||
processStartedAt: Date | null;
|
||||
retryOfRunId: string | null;
|
||||
processLossRetryCount: number;
|
||||
livenessState: RunLivenessState | null;
|
||||
livenessReason: string | null;
|
||||
continuationAttempt: number;
|
||||
lastUsefulActionAt: Date | null;
|
||||
nextAction: string | null;
|
||||
contextSnapshot: Record<string, unknown> | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ export {
|
|||
|
||||
export {
|
||||
createIssueSchema,
|
||||
createChildIssueSchema,
|
||||
createIssueLabelSchema,
|
||||
updateIssueSchema,
|
||||
issueExecutionPolicySchema,
|
||||
|
|
@ -148,6 +149,7 @@ export {
|
|||
upsertIssueDocumentSchema,
|
||||
restoreIssueDocumentRevisionSchema,
|
||||
type CreateIssue,
|
||||
type CreateChildIssue,
|
||||
type CreateIssueLabel,
|
||||
type UpdateIssue,
|
||||
type IssueExecutionWorkspaceSettings,
|
||||
|
|
|
|||
|
|
@ -138,6 +138,18 @@ export const createIssueSchema = z.object({
|
|||
|
||||
export type CreateIssue = z.infer<typeof createIssueSchema>;
|
||||
|
||||
export const createChildIssueSchema = createIssueSchema
|
||||
.omit({
|
||||
parentId: true,
|
||||
inheritExecutionWorkspaceFromIssueId: true,
|
||||
})
|
||||
.extend({
|
||||
acceptanceCriteria: z.array(z.string().trim().min(1).max(500)).max(20).optional(),
|
||||
blockParentUntilDone: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
export type CreateChildIssue = z.infer<typeof createChildIssueSchema>;
|
||||
|
||||
export const createIssueLabelSchema = z.object({
|
||||
name: z.string().trim().min(1).max(48),
|
||||
color: z.string().regex(/^#(?:[0-9a-fA-F]{6})$/, "Color must be a 6-digit hex value"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue