mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
Merge branch 'master' into fix/configurable-claimed-api-key-path
This commit is contained in:
commit
c171ff901c
165 changed files with 11349 additions and 1649 deletions
|
|
@ -22,6 +22,9 @@ export type {
|
|||
AdapterModel,
|
||||
HireApprovedPayload,
|
||||
HireApprovedHookResult,
|
||||
ConfigFieldOption,
|
||||
ConfigFieldSchema,
|
||||
AdapterConfigSchema,
|
||||
ServerAdapterModule,
|
||||
QuotaWindow,
|
||||
ProviderQuotaResult,
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ export function redactTranscriptEntryPaths(entry: TranscriptEntry, opts?: HomePa
|
|||
case "stderr":
|
||||
case "system":
|
||||
case "stdout":
|
||||
case "diff":
|
||||
return { ...entry, text: redactHomePathUserSegments(entry.text, opts) };
|
||||
case "tool_call":
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -193,6 +193,174 @@ export function joinPromptSections(
|
|||
.join(separator);
|
||||
}
|
||||
|
||||
type PaperclipWakeIssue = {
|
||||
id: string | null;
|
||||
identifier: string | null;
|
||||
title: string | null;
|
||||
status: string | null;
|
||||
priority: string | null;
|
||||
};
|
||||
|
||||
type PaperclipWakeComment = {
|
||||
id: string | null;
|
||||
issueId: string | null;
|
||||
body: string;
|
||||
bodyTruncated: boolean;
|
||||
createdAt: string | null;
|
||||
authorType: string | null;
|
||||
authorId: string | null;
|
||||
};
|
||||
|
||||
type PaperclipWakePayload = {
|
||||
reason: string | null;
|
||||
issue: PaperclipWakeIssue | null;
|
||||
commentIds: string[];
|
||||
latestCommentId: string | null;
|
||||
comments: PaperclipWakeComment[];
|
||||
requestedCount: number;
|
||||
includedCount: number;
|
||||
missingCount: number;
|
||||
truncated: boolean;
|
||||
fallbackFetchNeeded: boolean;
|
||||
};
|
||||
|
||||
function normalizePaperclipWakeIssue(value: unknown): PaperclipWakeIssue | null {
|
||||
const issue = parseObject(value);
|
||||
const id = asString(issue.id, "").trim() || null;
|
||||
const identifier = asString(issue.identifier, "").trim() || null;
|
||||
const title = asString(issue.title, "").trim() || null;
|
||||
const status = asString(issue.status, "").trim() || null;
|
||||
const priority = asString(issue.priority, "").trim() || null;
|
||||
if (!id && !identifier && !title) return null;
|
||||
return {
|
||||
id,
|
||||
identifier,
|
||||
title,
|
||||
status,
|
||||
priority,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePaperclipWakeComment(value: unknown): PaperclipWakeComment | null {
|
||||
const comment = parseObject(value);
|
||||
const author = parseObject(comment.author);
|
||||
const body = asString(comment.body, "");
|
||||
if (!body.trim()) return null;
|
||||
return {
|
||||
id: asString(comment.id, "").trim() || null,
|
||||
issueId: asString(comment.issueId, "").trim() || null,
|
||||
body,
|
||||
bodyTruncated: asBoolean(comment.bodyTruncated, false),
|
||||
createdAt: asString(comment.createdAt, "").trim() || null,
|
||||
authorType: asString(author.type, "").trim() || null,
|
||||
authorId: asString(author.id, "").trim() || null,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizePaperclipWakePayload(value: unknown): PaperclipWakePayload | null {
|
||||
const payload = parseObject(value);
|
||||
const comments = Array.isArray(payload.comments)
|
||||
? payload.comments
|
||||
.map((entry) => normalizePaperclipWakeComment(entry))
|
||||
.filter((entry): entry is PaperclipWakeComment => Boolean(entry))
|
||||
: [];
|
||||
const commentWindow = parseObject(payload.commentWindow);
|
||||
const commentIds = Array.isArray(payload.commentIds)
|
||||
? payload.commentIds
|
||||
.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
|
||||
.map((entry) => entry.trim())
|
||||
: [];
|
||||
|
||||
if (comments.length === 0 && commentIds.length === 0) return null;
|
||||
|
||||
return {
|
||||
reason: asString(payload.reason, "").trim() || null,
|
||||
issue: normalizePaperclipWakeIssue(payload.issue),
|
||||
commentIds,
|
||||
latestCommentId: asString(payload.latestCommentId, "").trim() || null,
|
||||
comments,
|
||||
requestedCount: asNumber(commentWindow.requestedCount, comments.length || commentIds.length),
|
||||
includedCount: asNumber(commentWindow.includedCount, comments.length),
|
||||
missingCount: asNumber(commentWindow.missingCount, 0),
|
||||
truncated: asBoolean(payload.truncated, false),
|
||||
fallbackFetchNeeded: asBoolean(payload.fallbackFetchNeeded, false),
|
||||
};
|
||||
}
|
||||
|
||||
export function stringifyPaperclipWakePayload(value: unknown): string | null {
|
||||
const normalized = normalizePaperclipWakePayload(value);
|
||||
if (!normalized) return null;
|
||||
return JSON.stringify(normalized);
|
||||
}
|
||||
|
||||
export function renderPaperclipWakePrompt(
|
||||
value: unknown,
|
||||
options: { resumedSession?: boolean } = {},
|
||||
): string {
|
||||
const normalized = normalizePaperclipWakePayload(value);
|
||||
if (!normalized) return "";
|
||||
const resumedSession = options.resumedSession === true;
|
||||
|
||||
const lines = resumedSession
|
||||
? [
|
||||
"## Paperclip Resume Delta",
|
||||
"",
|
||||
"You are resuming an existing Paperclip session.",
|
||||
"This heartbeat is scoped to the issue below. Do not switch to another issue until you have handled this wake.",
|
||||
"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.",
|
||||
"",
|
||||
`- reason: ${normalized.reason ?? "unknown"}`,
|
||||
`- issue: ${normalized.issue?.identifier ?? normalized.issue?.id ?? "unknown"}${normalized.issue?.title ? ` ${normalized.issue.title}` : ""}`,
|
||||
`- pending comments: ${normalized.includedCount}/${normalized.requestedCount}`,
|
||||
`- latest comment id: ${normalized.latestCommentId ?? "unknown"}`,
|
||||
`- fallback fetch needed: ${normalized.fallbackFetchNeeded ? "yes" : "no"}`,
|
||||
]
|
||||
: [
|
||||
"## Paperclip Wake Payload",
|
||||
"",
|
||||
"Treat this wake payload as the highest-priority change for the current heartbeat.",
|
||||
"This heartbeat is scoped to the issue below. Do not switch to another issue until you have handled this wake.",
|
||||
"Before generic repo exploration or boilerplate heartbeat updates, acknowledge the latest comment and explain how it changes your next action.",
|
||||
"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.",
|
||||
"",
|
||||
`- reason: ${normalized.reason ?? "unknown"}`,
|
||||
`- issue: ${normalized.issue?.identifier ?? normalized.issue?.id ?? "unknown"}${normalized.issue?.title ? ` ${normalized.issue.title}` : ""}`,
|
||||
`- pending comments: ${normalized.includedCount}/${normalized.requestedCount}`,
|
||||
`- latest comment id: ${normalized.latestCommentId ?? "unknown"}`,
|
||||
`- fallback fetch needed: ${normalized.fallbackFetchNeeded ? "yes" : "no"}`,
|
||||
];
|
||||
|
||||
if (normalized.issue?.status) {
|
||||
lines.push(`- issue status: ${normalized.issue.status}`);
|
||||
}
|
||||
if (normalized.issue?.priority) {
|
||||
lines.push(`- issue priority: ${normalized.issue.priority}`);
|
||||
}
|
||||
if (normalized.missingCount > 0) {
|
||||
lines.push(`- omitted comments: ${normalized.missingCount}`);
|
||||
}
|
||||
|
||||
lines.push("", "New comments in order:");
|
||||
|
||||
for (const [index, comment] of normalized.comments.entries()) {
|
||||
const authorLabel = comment.authorId
|
||||
? `${comment.authorType ?? "unknown"} ${comment.authorId}`
|
||||
: comment.authorType ?? "unknown";
|
||||
lines.push(
|
||||
`${index + 1}. comment ${comment.id ?? "unknown"} at ${comment.createdAt ?? "unknown"} by ${authorLabel}`,
|
||||
comment.body,
|
||||
);
|
||||
if (comment.bodyTruncated) {
|
||||
lines.push("[comment body truncated]");
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
return lines.join("\n").trim();
|
||||
}
|
||||
|
||||
export function redactEnvForLogs(env: Record<string, string>): Record<string, string> {
|
||||
const redacted: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export const LEGACY_SESSIONED_ADAPTER_TYPES = new Set([
|
|||
"codex_local",
|
||||
"cursor",
|
||||
"gemini_local",
|
||||
"hermes_local",
|
||||
"opencode_local",
|
||||
"pi_local",
|
||||
]);
|
||||
|
|
@ -76,6 +77,11 @@ export const ADAPTER_SESSION_MANAGEMENT: Record<string, AdapterSessionManagement
|
|||
nativeContextManagement: "unknown",
|
||||
defaultSessionCompaction: DEFAULT_SESSION_COMPACTION_POLICY,
|
||||
},
|
||||
hermes_local: {
|
||||
supportsSessionResume: true,
|
||||
nativeContextManagement: "confirmed",
|
||||
defaultSessionCompaction: ADAPTER_MANAGED_SESSION_POLICY,
|
||||
},
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
|
|
|
|||
|
|
@ -261,6 +261,34 @@ export interface ProviderQuotaResult {
|
|||
windows: QuotaWindow[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Adapter config schema — declarative UI config for external adapters
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ConfigFieldOption {
|
||||
label: string;
|
||||
value: string;
|
||||
/** Optional group key for categorizing options (e.g. provider name) */
|
||||
group?: string;
|
||||
}
|
||||
|
||||
export interface ConfigFieldSchema {
|
||||
key: string;
|
||||
label: string;
|
||||
type: "text" | "select" | "toggle" | "number" | "textarea" | "combobox";
|
||||
options?: ConfigFieldOption[];
|
||||
default?: unknown;
|
||||
hint?: string;
|
||||
required?: boolean;
|
||||
group?: string;
|
||||
/** Optional metadata — not rendered, but available to custom UI logic */
|
||||
meta?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AdapterConfigSchema {
|
||||
fields: ConfigFieldSchema[];
|
||||
}
|
||||
|
||||
export interface ServerAdapterModule {
|
||||
type: string;
|
||||
execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult>;
|
||||
|
|
@ -292,7 +320,14 @@ export interface ServerAdapterModule {
|
|||
* Returns the detected model/provider and the config source, or null if
|
||||
* the adapter does not support detection or no config is found.
|
||||
*/
|
||||
detectModel?: () => Promise<{ model: string; provider: string; source: string } | null>;
|
||||
detectModel?: () => Promise<{ model: string; provider: string; source: string; candidates?: string[] } | null>;
|
||||
/**
|
||||
* Optional: return a declarative config schema so the UI can render
|
||||
* adapter-specific form fields without shipping React components.
|
||||
* Dynamic options (e.g. scanning a profiles directory) should be
|
||||
* resolved inside this method — the caller receives a fully hydrated schema.
|
||||
*/
|
||||
getConfigSchema?: () => Promise<AdapterConfigSchema> | AdapterConfigSchema;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -309,7 +344,8 @@ export type TranscriptEntry =
|
|||
| { kind: "result"; ts: string; text: string; inputTokens: number; outputTokens: number; cachedTokens: number; costUsd: number; subtype: string; isError: boolean; errors: string[] }
|
||||
| { kind: "stderr"; ts: string; text: string }
|
||||
| { kind: "system"; ts: string; text: string }
|
||||
| { kind: "stdout"; ts: string; text: string };
|
||||
| { kind: "stdout"; ts: string; text: string }
|
||||
| { kind: "diff"; ts: string; changeType: "add" | "remove" | "context" | "hunk" | "file_header" | "truncation"; text: string };
|
||||
|
||||
export type StdoutLineParser = (line: string, ts: string) => TranscriptEntry[];
|
||||
|
||||
|
|
@ -353,4 +389,6 @@ export interface CreateConfigValues {
|
|||
maxTurnsPerRun: number;
|
||||
heartbeatEnabled: boolean;
|
||||
intervalSec: number;
|
||||
/** Arbitrary key-value pairs populated by schema-driven config fields. */
|
||||
adapterSchemaValues?: Record<string, unknown>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Core fields:
|
|||
- chrome (boolean, optional): pass --chrome when running Claude
|
||||
- promptTemplate (string, optional): run prompt template
|
||||
- maxTurnsPerRun (number, optional): max turns for one run
|
||||
- dangerouslySkipPermissions (boolean, optional): pass --dangerously-skip-permissions to claude
|
||||
- dangerouslySkipPermissions (boolean, optional, default true): pass --dangerously-skip-permissions to claude; defaults to true because Paperclip runs Claude in headless --print mode where interactive permission prompts cannot be answered
|
||||
- command (string, optional): defaults to "claude"
|
||||
- extraArgs (string[], optional): additional CLI args
|
||||
- env (object, optional): KEY=VALUE environment variables
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import {
|
|||
ensurePathInEnv,
|
||||
resolveCommandForLogs,
|
||||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
stringifyPaperclipWakePayload,
|
||||
runChildProcess,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import {
|
||||
|
|
@ -170,6 +172,7 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
|
|||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
|
||||
|
||||
if (wakeTaskId) {
|
||||
env.PAPERCLIP_TASK_ID = wakeTaskId;
|
||||
|
|
@ -189,6 +192,9 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
|
|||
if (linkedIssueIds.length > 0) {
|
||||
env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
}
|
||||
if (wakePayloadJson) {
|
||||
env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
|
||||
}
|
||||
if (effectiveWorkspaceCwd) {
|
||||
env.PAPERCLIP_WORKSPACE_CWD = effectiveWorkspaceCwd;
|
||||
}
|
||||
|
|
@ -317,7 +323,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
const effort = asString(config.effort, "");
|
||||
const chrome = asBoolean(config.chrome, false);
|
||||
const maxTurns = asNumber(config.maxTurnsPerRun, 0);
|
||||
const dangerouslySkipPermissions = asBoolean(config.dangerouslySkipPermissions, false);
|
||||
const dangerouslySkipPermissions = asBoolean(config.dangerouslySkipPermissions, true);
|
||||
const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
|
||||
const instructionsFileDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : "";
|
||||
const commandNotes = instructionsFilePath
|
||||
|
|
@ -398,20 +404,24 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
run: { id: runId, source: "on_demand" },
|
||||
context,
|
||||
};
|
||||
const renderedPrompt = renderTemplate(promptTemplate, templateData);
|
||||
const renderedBootstrapPrompt =
|
||||
!sessionId && bootstrapPromptTemplate.trim().length > 0
|
||||
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
||||
: "";
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) });
|
||||
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
|
||||
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
|
||||
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
||||
const prompt = joinPromptSections([
|
||||
renderedBootstrapPrompt,
|
||||
wakePrompt,
|
||||
sessionHandoffNote,
|
||||
renderedPrompt,
|
||||
]);
|
||||
const promptMetrics = {
|
||||
promptChars: prompt.length,
|
||||
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
||||
wakePromptChars: wakePrompt.length,
|
||||
sessionHandoffChars: sessionHandoffNote.length,
|
||||
heartbeatPromptChars: renderedPrompt.length,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ export async function testEnvironment(
|
|||
const effort = asString(config.effort, "").trim();
|
||||
const chrome = asBoolean(config.chrome, false);
|
||||
const maxTurns = asNumber(config.maxTurnsPerRun, 0);
|
||||
const dangerouslySkipPermissions = asBoolean(config.dangerouslySkipPermissions, false);
|
||||
const dangerouslySkipPermissions = asBoolean(config.dangerouslySkipPermissions, true);
|
||||
const extraArgs = (() => {
|
||||
const fromExtraArgs = asStringArray(config.extraArgs);
|
||||
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import {
|
|||
resolveCommandForLogs,
|
||||
resolvePaperclipDesiredSkillNames,
|
||||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
stringifyPaperclipWakePayload,
|
||||
joinPromptSections,
|
||||
runChildProcess,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
|
|
@ -313,6 +315,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
|
||||
if (wakeTaskId) {
|
||||
env.PAPERCLIP_TASK_ID = wakeTaskId;
|
||||
}
|
||||
|
|
@ -331,6 +334,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
if (linkedIssueIds.length > 0) {
|
||||
env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
}
|
||||
if (wakePayloadJson) {
|
||||
env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
|
||||
}
|
||||
if (effectiveWorkspaceCwd) {
|
||||
env.PAPERCLIP_WORKSPACE_CWD = effectiveWorkspaceCwd;
|
||||
}
|
||||
|
|
@ -434,11 +440,36 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
}
|
||||
const repoAgentsNote =
|
||||
"Codex exec automatically applies repo-scoped AGENTS.md instructions from the current workspace; Paperclip does not currently suppress that discovery.";
|
||||
const bootstrapPromptTemplate = asString(config.bootstrapPromptTemplate, "");
|
||||
const templateData = {
|
||||
agentId: agent.id,
|
||||
companyId: agent.companyId,
|
||||
runId,
|
||||
company: { id: agent.companyId },
|
||||
agent,
|
||||
run: { id: runId, source: "on_demand" },
|
||||
context,
|
||||
};
|
||||
const renderedBootstrapPrompt =
|
||||
!sessionId && bootstrapPromptTemplate.trim().length > 0
|
||||
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
||||
: "";
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) });
|
||||
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
|
||||
const promptInstructionsPrefix = shouldUseResumeDeltaPrompt ? "" : instructionsPrefix;
|
||||
instructionsChars = promptInstructionsPrefix.length;
|
||||
const commandNotes = (() => {
|
||||
if (!instructionsFilePath) {
|
||||
return [repoAgentsNote];
|
||||
}
|
||||
if (instructionsPrefix.length > 0) {
|
||||
if (shouldUseResumeDeltaPrompt) {
|
||||
return [
|
||||
`Loaded agent instructions from ${instructionsFilePath}`,
|
||||
"Skipped stdin instruction reinjection because an existing Codex session is being resumed with a wake delta.",
|
||||
repoAgentsNote,
|
||||
];
|
||||
}
|
||||
return [
|
||||
`Loaded agent instructions from ${instructionsFilePath}`,
|
||||
`Prepended instructions + path directive to stdin prompt (relative references from ${instructionsDir}).`,
|
||||
|
|
@ -450,25 +481,12 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
repoAgentsNote,
|
||||
];
|
||||
})();
|
||||
const bootstrapPromptTemplate = asString(config.bootstrapPromptTemplate, "");
|
||||
const templateData = {
|
||||
agentId: agent.id,
|
||||
companyId: agent.companyId,
|
||||
runId,
|
||||
company: { id: agent.companyId },
|
||||
agent,
|
||||
run: { id: runId, source: "on_demand" },
|
||||
context,
|
||||
};
|
||||
const renderedPrompt = renderTemplate(promptTemplate, templateData);
|
||||
const renderedBootstrapPrompt =
|
||||
!sessionId && bootstrapPromptTemplate.trim().length > 0
|
||||
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
||||
: "";
|
||||
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
|
||||
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
||||
const prompt = joinPromptSections([
|
||||
instructionsPrefix,
|
||||
promptInstructionsPrefix,
|
||||
renderedBootstrapPrompt,
|
||||
wakePrompt,
|
||||
sessionHandoffNote,
|
||||
renderedPrompt,
|
||||
]);
|
||||
|
|
@ -476,6 +494,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
promptChars: prompt.length,
|
||||
instructionsChars,
|
||||
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
||||
wakePromptChars: wakePrompt.length,
|
||||
sessionHandoffChars: sessionHandoffNote.length,
|
||||
heartbeatPromptChars: renderedPrompt.length,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import {
|
|||
resolvePaperclipDesiredSkillNames,
|
||||
removeMaintainerOnlySkillSymlinks,
|
||||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
stringifyPaperclipWakePayload,
|
||||
joinPromptSections,
|
||||
runChildProcess,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
|
|
@ -219,6 +221,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
|
||||
if (wakeTaskId) {
|
||||
env.PAPERCLIP_TASK_ID = wakeTaskId;
|
||||
}
|
||||
|
|
@ -237,6 +240,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
if (linkedIssueIds.length > 0) {
|
||||
env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
}
|
||||
if (wakePayloadJson) {
|
||||
env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
|
||||
}
|
||||
if (effectiveWorkspaceCwd) {
|
||||
env.PAPERCLIP_WORKSPACE_CWD = effectiveWorkspaceCwd;
|
||||
}
|
||||
|
|
@ -352,16 +358,19 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
run: { id: runId, source: "on_demand" },
|
||||
context,
|
||||
};
|
||||
const renderedPrompt = renderTemplate(promptTemplate, templateData);
|
||||
const renderedBootstrapPrompt =
|
||||
!sessionId && bootstrapPromptTemplate.trim().length > 0
|
||||
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
||||
: "";
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) });
|
||||
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
|
||||
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
|
||||
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
||||
const paperclipEnvNote = renderPaperclipEnvNote(env);
|
||||
const prompt = joinPromptSections([
|
||||
instructionsPrefix,
|
||||
renderedBootstrapPrompt,
|
||||
wakePrompt,
|
||||
sessionHandoffNote,
|
||||
paperclipEnvNote,
|
||||
renderedPrompt,
|
||||
|
|
@ -370,6 +379,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
promptChars: prompt.length,
|
||||
instructionsChars,
|
||||
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
||||
wakePromptChars: wakePrompt.length,
|
||||
sessionHandoffChars: sessionHandoffNote.length,
|
||||
runtimeNoteChars: paperclipEnvNote.length,
|
||||
heartbeatPromptChars: renderedPrompt.length,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import {
|
|||
removeMaintainerOnlySkillSymlinks,
|
||||
parseObject,
|
||||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
stringifyPaperclipWakePayload,
|
||||
runChildProcess,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import { DEFAULT_GEMINI_LOCAL_MODEL } from "../index.js";
|
||||
|
|
@ -193,12 +195,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
|
||||
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
|
||||
if (wakeReason) env.PAPERCLIP_WAKE_REASON = wakeReason;
|
||||
if (wakeCommentId) env.PAPERCLIP_WAKE_COMMENT_ID = wakeCommentId;
|
||||
if (approvalId) env.PAPERCLIP_APPROVAL_ID = approvalId;
|
||||
if (approvalStatus) env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
|
||||
if (linkedIssueIds.length > 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
|
||||
if (effectiveWorkspaceCwd) env.PAPERCLIP_WORKSPACE_CWD = effectiveWorkspaceCwd;
|
||||
if (workspaceSource) env.PAPERCLIP_WORKSPACE_SOURCE = workspaceSource;
|
||||
if (workspaceId) env.PAPERCLIP_WORKSPACE_ID = workspaceId;
|
||||
|
|
@ -295,17 +299,20 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
run: { id: runId, source: "on_demand" },
|
||||
context,
|
||||
};
|
||||
const renderedPrompt = renderTemplate(promptTemplate, templateData);
|
||||
const renderedBootstrapPrompt =
|
||||
!sessionId && bootstrapPromptTemplate.trim().length > 0
|
||||
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
||||
: "";
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) });
|
||||
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
|
||||
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
|
||||
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
||||
const paperclipEnvNote = renderPaperclipEnvNote(env);
|
||||
const apiAccessNote = renderApiAccessNote(env);
|
||||
const prompt = joinPromptSections([
|
||||
instructionsPrefix,
|
||||
renderedBootstrapPrompt,
|
||||
wakePrompt,
|
||||
sessionHandoffNote,
|
||||
paperclipEnvNote,
|
||||
apiAccessNote,
|
||||
|
|
@ -315,6 +322,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
promptChars: prompt.length,
|
||||
instructionsChars: instructionsPrefix.length,
|
||||
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
||||
wakePromptChars: wakePrompt.length,
|
||||
sessionHandoffChars: sessionHandoffNote.length,
|
||||
runtimeNoteChars: paperclipEnvNote.length + apiAccessNote.length,
|
||||
heartbeatPromptChars: renderedPrompt.length,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,14 @@ import type {
|
|||
AdapterExecutionResult,
|
||||
AdapterRuntimeServiceReport,
|
||||
} from "@paperclipai/adapter-utils";
|
||||
import { asNumber, asString, buildPaperclipEnv, parseObject } from "@paperclipai/adapter-utils/server-utils";
|
||||
import {
|
||||
asNumber,
|
||||
asString,
|
||||
buildPaperclipEnv,
|
||||
parseObject,
|
||||
renderPaperclipWakePrompt,
|
||||
stringifyPaperclipWakePayload,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import crypto, { randomUUID } from "node:crypto";
|
||||
import { WebSocket } from "ws";
|
||||
|
||||
|
|
@ -341,7 +348,12 @@ function buildPaperclipEnvForWake(ctx: AdapterExecutionContext, wakePayload: Wak
|
|||
return paperclipEnv;
|
||||
}
|
||||
|
||||
function buildWakeText(payload: WakePayload, paperclipEnv: Record<string, string>, claimedApiKeyPath: string): string {
|
||||
function buildWakeText(
|
||||
payload: WakePayload,
|
||||
paperclipEnv: Record<string, string>,
|
||||
structuredWakePrompt: string,
|
||||
): string {
|
||||
const claimedApiKeyPath = "~/.openclaw/workspace/paperclip-claimed-api-key.json";
|
||||
const orderedKeys = [
|
||||
"PAPERCLIP_RUN_ID",
|
||||
"PAPERCLIP_AGENT_ID",
|
||||
|
|
@ -409,6 +421,12 @@ function buildWakeText(payload: WakePayload, paperclipEnv: Record<string, string
|
|||
"- POST /api/issues/{issueId}/comments",
|
||||
"- PATCH /api/issues/{issueId}",
|
||||
"- POST /api/companies/{companyId}/issues (when asked to create a new issue)",
|
||||
...(structuredWakePrompt
|
||||
? [
|
||||
"",
|
||||
structuredWakePrompt,
|
||||
]
|
||||
: []),
|
||||
"",
|
||||
"Complete the workflow in this run.",
|
||||
];
|
||||
|
|
@ -420,6 +438,17 @@ function appendWakeText(baseText: string, wakeText: string): string {
|
|||
return trimmedBase.length > 0 ? `${trimmedBase}\n\n${wakeText}` : wakeText;
|
||||
}
|
||||
|
||||
function joinWakePayloadSections(structuredWakePrompt: string, structuredWakeJson: string): string {
|
||||
const sections = [
|
||||
structuredWakePrompt.trim(),
|
||||
"Structured wake payload JSON:",
|
||||
"```json",
|
||||
structuredWakeJson,
|
||||
"```",
|
||||
].filter((entry) => entry.trim().length > 0);
|
||||
return sections.join("\n");
|
||||
}
|
||||
|
||||
function buildStandardPaperclipPayload(
|
||||
ctx: AdapterExecutionContext,
|
||||
wakePayload: WakePayload,
|
||||
|
|
@ -452,6 +481,10 @@ function buildStandardPaperclipPayload(
|
|||
approvalStatus: wakePayload.approvalStatus,
|
||||
apiUrl: paperclipEnv.PAPERCLIP_API_URL ?? null,
|
||||
};
|
||||
const structuredWake = parseObject(ctx.context.paperclipWake);
|
||||
if (Object.keys(structuredWake).length > 0) {
|
||||
standardPaperclip.wake = structuredWake;
|
||||
}
|
||||
|
||||
if (workspace) {
|
||||
standardPaperclip.workspace = workspace;
|
||||
|
|
@ -1058,8 +1091,15 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
|
||||
const wakePayload = buildWakePayload(ctx);
|
||||
const paperclipEnv = buildPaperclipEnvForWake(ctx, wakePayload);
|
||||
const claimedApiKeyPath = resolveClaimedApiKeyPath(ctx.config.claimedApiKeyPath);
|
||||
const wakeText = buildWakeText(wakePayload, paperclipEnv, claimedApiKeyPath);
|
||||
const structuredWakePrompt = renderPaperclipWakePrompt(ctx.context.paperclipWake);
|
||||
const structuredWakeJson = stringifyPaperclipWakePayload(ctx.context.paperclipWake);
|
||||
const wakeText = buildWakeText(
|
||||
wakePayload,
|
||||
paperclipEnv,
|
||||
structuredWakeJson
|
||||
? joinWakePayloadSections(structuredWakePrompt, structuredWakeJson)
|
||||
: structuredWakePrompt,
|
||||
);
|
||||
|
||||
const sessionKeyStrategy = normalizeSessionKeyStrategy(ctx.config.sessionKeyStrategy);
|
||||
const configuredSessionKey = nonEmpty(ctx.config.sessionKey);
|
||||
|
|
@ -1081,6 +1121,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
idempotencyKey: ctx.runId,
|
||||
};
|
||||
delete agentParams.text;
|
||||
agentParams.paperclip = paperclipPayload;
|
||||
|
||||
const configuredAgentId = nonEmpty(ctx.config.agentId);
|
||||
if (configuredAgentId && !nonEmpty(agentParams.agentId)) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import {
|
|||
ensurePathInEnv,
|
||||
resolveCommandForLogs,
|
||||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
stringifyPaperclipWakePayload,
|
||||
runChildProcess,
|
||||
readPaperclipRuntimeSkillEntries,
|
||||
resolvePaperclipDesiredSkillNames,
|
||||
|
|
@ -154,12 +156,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
|
||||
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
|
||||
if (wakeReason) env.PAPERCLIP_WAKE_REASON = wakeReason;
|
||||
if (wakeCommentId) env.PAPERCLIP_WAKE_COMMENT_ID = wakeCommentId;
|
||||
if (approvalId) env.PAPERCLIP_APPROVAL_ID = approvalId;
|
||||
if (approvalStatus) env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
|
||||
if (linkedIssueIds.length > 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
|
||||
if (effectiveWorkspaceCwd) env.PAPERCLIP_WORKSPACE_CWD = effectiveWorkspaceCwd;
|
||||
if (workspaceSource) env.PAPERCLIP_WORKSPACE_SOURCE = workspaceSource;
|
||||
if (workspaceId) env.PAPERCLIP_WORKSPACE_ID = workspaceId;
|
||||
|
|
@ -222,7 +226,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
`[paperclip] OpenCode session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${cwd}".\n`,
|
||||
);
|
||||
}
|
||||
|
||||
const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
|
||||
const resolvedInstructionsFilePath = instructionsFilePath
|
||||
? path.resolve(cwd, instructionsFilePath)
|
||||
|
|
@ -271,15 +274,18 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
run: { id: runId, source: "on_demand" },
|
||||
context,
|
||||
};
|
||||
const renderedPrompt = renderTemplate(promptTemplate, templateData);
|
||||
const renderedBootstrapPrompt =
|
||||
!sessionId && bootstrapPromptTemplate.trim().length > 0
|
||||
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
||||
: "";
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) });
|
||||
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
|
||||
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
|
||||
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
||||
const prompt = joinPromptSections([
|
||||
instructionsPrefix,
|
||||
renderedBootstrapPrompt,
|
||||
wakePrompt,
|
||||
sessionHandoffNote,
|
||||
renderedPrompt,
|
||||
]);
|
||||
|
|
@ -287,6 +293,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
promptChars: prompt.length,
|
||||
instructionsChars: instructionsPrefix.length,
|
||||
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
||||
wakePromptChars: wakePrompt.length,
|
||||
sessionHandoffChars: sessionHandoffNote.length,
|
||||
heartbeatPromptChars: renderedPrompt.length,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import {
|
|||
resolvePaperclipDesiredSkillNames,
|
||||
removeMaintainerOnlySkillSymlinks,
|
||||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
stringifyPaperclipWakePayload,
|
||||
runChildProcess,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import { isPiUnknownSessionError, parsePiJsonl } from "./parse.js";
|
||||
|
|
@ -177,6 +179,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
|
||||
|
||||
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
|
||||
if (wakeReason) env.PAPERCLIP_WAKE_REASON = wakeReason;
|
||||
|
|
@ -184,6 +187,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
if (approvalId) env.PAPERCLIP_APPROVAL_ID = approvalId;
|
||||
if (approvalStatus) env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
|
||||
if (linkedIssueIds.length > 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
|
||||
if (workspaceCwd) env.PAPERCLIP_WORKSPACE_CWD = workspaceCwd;
|
||||
if (workspaceSource) env.PAPERCLIP_WORKSPACE_SOURCE = workspaceSource;
|
||||
if (workspaceId) env.PAPERCLIP_WORKSPACE_ID = workspaceId;
|
||||
|
|
@ -298,14 +302,17 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
context,
|
||||
};
|
||||
const renderedSystemPromptExtension = renderTemplate(systemPromptExtension, templateData);
|
||||
const renderedHeartbeatPrompt = renderTemplate(promptTemplate, templateData);
|
||||
const renderedBootstrapPrompt =
|
||||
!canResumeSession && bootstrapPromptTemplate.trim().length > 0
|
||||
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
||||
: "";
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: canResumeSession });
|
||||
const shouldUseResumeDeltaPrompt = canResumeSession && wakePrompt.length > 0;
|
||||
const renderedHeartbeatPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
|
||||
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
||||
const userPrompt = joinPromptSections([
|
||||
renderedBootstrapPrompt,
|
||||
wakePrompt,
|
||||
sessionHandoffNote,
|
||||
renderedHeartbeatPrompt,
|
||||
]);
|
||||
|
|
@ -313,6 +320,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
systemPromptChars: renderedSystemPromptExtension.length,
|
||||
promptChars: userPrompt.length,
|
||||
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
||||
wakePromptChars: wakePrompt.length,
|
||||
sessionHandoffChars: sessionHandoffNote.length,
|
||||
heartbeatPromptChars: renderedHeartbeatPrompt.length,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -606,7 +606,7 @@ export interface WorkerToHostMethods {
|
|||
result: IssueComment[],
|
||||
];
|
||||
"issues.createComment": [
|
||||
params: { issueId: string; body: string; companyId: string },
|
||||
params: { issueId: string; body: string; companyId: string; authorAgentId?: string },
|
||||
result: IssueComment,
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -405,7 +405,7 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness {
|
|||
if (!isInCompany(issues.get(issueId), companyId)) return [];
|
||||
return issueComments.get(issueId) ?? [];
|
||||
},
|
||||
async createComment(issueId, body, companyId) {
|
||||
async createComment(issueId, body, companyId, options) {
|
||||
requireCapability(manifest, capabilitySet, "issue.comments.create");
|
||||
const parentIssue = issues.get(issueId);
|
||||
if (!isInCompany(parentIssue, companyId)) {
|
||||
|
|
@ -416,7 +416,7 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness {
|
|||
id: randomUUID(),
|
||||
companyId: parentIssue.companyId,
|
||||
issueId,
|
||||
authorAgentId: null,
|
||||
authorAgentId: options?.authorAgentId ?? null,
|
||||
authorUserId: null,
|
||||
body,
|
||||
createdAt: now,
|
||||
|
|
|
|||
|
|
@ -909,7 +909,12 @@ export interface PluginIssuesClient {
|
|||
companyId: string,
|
||||
): Promise<Issue>;
|
||||
listComments(issueId: string, companyId: string): Promise<IssueComment[]>;
|
||||
createComment(issueId: string, body: string, companyId: string): Promise<IssueComment>;
|
||||
createComment(
|
||||
issueId: string,
|
||||
body: string,
|
||||
companyId: string,
|
||||
options?: { authorAgentId?: string },
|
||||
): Promise<IssueComment>;
|
||||
/** Read and write issue documents. Requires `issue.documents.read` / `issue.documents.write`. */
|
||||
documents: PluginIssueDocumentsClient;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -610,8 +610,8 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost
|
|||
return callHost("issues.listComments", { issueId, companyId });
|
||||
},
|
||||
|
||||
async createComment(issueId: string, body: string, companyId: string) {
|
||||
return callHost("issues.createComment", { issueId, body, companyId });
|
||||
async createComment(issueId: string, body: string, companyId: string, options?: { authorAgentId?: string }) {
|
||||
return callHost("issues.createComment", { issueId, body, companyId, authorAgentId: options?.authorAgentId });
|
||||
},
|
||||
|
||||
documents: {
|
||||
|
|
|
|||
15
packages/shared/src/adapter-type.ts
Normal file
15
packages/shared/src/adapter-type.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { z } from "zod";
|
||||
import { AGENT_ADAPTER_TYPES } from "./constants.js";
|
||||
|
||||
export const agentAdapterTypeSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.default("process")
|
||||
.describe(`Known built-in adapters: ${AGENT_ADAPTER_TYPES.join(", ")}. External adapters may register additional non-empty string types at runtime.`);
|
||||
|
||||
export const optionalAgentAdapterTypeSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.optional();
|
||||
38
packages/shared/src/adapter-types.test.ts
Normal file
38
packages/shared/src/adapter-types.test.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { acceptInviteSchema, createAgentSchema, updateAgentSchema } from "./index.js";
|
||||
|
||||
describe("dynamic adapter type validation schemas", () => {
|
||||
it("accepts external adapter types in create/update agent schemas", () => {
|
||||
expect(
|
||||
createAgentSchema.parse({
|
||||
name: "External Agent",
|
||||
adapterType: "external_adapter",
|
||||
}).adapterType,
|
||||
).toBe("external_adapter");
|
||||
|
||||
expect(
|
||||
updateAgentSchema.parse({
|
||||
adapterType: "external_adapter",
|
||||
}).adapterType,
|
||||
).toBe("external_adapter");
|
||||
});
|
||||
|
||||
it("still rejects blank adapter types", () => {
|
||||
expect(() =>
|
||||
createAgentSchema.parse({
|
||||
name: "Blank Adapter",
|
||||
adapterType: " ",
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("accepts external adapter types in invite acceptance schema", () => {
|
||||
expect(
|
||||
acceptInviteSchema.parse({
|
||||
requestType: "agent",
|
||||
agentName: "External Joiner",
|
||||
adapterType: "external_adapter",
|
||||
}).adapterType,
|
||||
).toBe("external_adapter");
|
||||
});
|
||||
});
|
||||
|
|
@ -31,9 +31,8 @@ export const AGENT_ADAPTER_TYPES = [
|
|||
"pi_local",
|
||||
"cursor",
|
||||
"openclaw_gateway",
|
||||
"hermes_local",
|
||||
] as const;
|
||||
export type AgentAdapterType = (typeof AGENT_ADAPTER_TYPES)[number];
|
||||
export type AgentAdapterType = (typeof AGENT_ADAPTER_TYPES)[number] | (string & {});
|
||||
|
||||
export const AGENT_ROLES = [
|
||||
"ceo",
|
||||
|
|
|
|||
19
packages/shared/src/execution-workspace-guards.ts
Normal file
19
packages/shared/src/execution-workspace-guards.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { ExecutionWorkspace } from "./types/workspace-runtime.js";
|
||||
|
||||
type ExecutionWorkspaceGuardTarget = Pick<ExecutionWorkspace, "closedAt" | "mode" | "name" | "status">;
|
||||
|
||||
const CLOSED_EXECUTION_WORKSPACE_STATUSES = new Set<ExecutionWorkspace["status"]>(["archived", "cleanup_failed"]);
|
||||
|
||||
export function isClosedIsolatedExecutionWorkspace(
|
||||
workspace: Pick<ExecutionWorkspaceGuardTarget, "closedAt" | "mode" | "status"> | null | undefined,
|
||||
): boolean {
|
||||
if (!workspace) return false;
|
||||
if (workspace.mode !== "isolated_workspace") return false;
|
||||
return workspace.closedAt != null || CLOSED_EXECUTION_WORKSPACE_STATUSES.has(workspace.status);
|
||||
}
|
||||
|
||||
export function getClosedIsolatedExecutionWorkspaceMessage(
|
||||
workspace: Pick<ExecutionWorkspaceGuardTarget, "name">,
|
||||
): string {
|
||||
return `This issue is linked to the closed workspace "${workspace.name}". Move it to an open workspace before adding comments or resuming work.`;
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export { agentAdapterTypeSchema, optionalAgentAdapterTypeSchema } from "./adapter-type.js";
|
||||
export {
|
||||
COMPANY_STATUSES,
|
||||
DEPLOYMENT_MODES,
|
||||
|
|
@ -350,6 +351,11 @@ export {
|
|||
DEFAULT_FEEDBACK_DATA_SHARING_TERMS_VERSION,
|
||||
} from "./types/feedback.js";
|
||||
|
||||
export {
|
||||
getClosedIsolatedExecutionWorkspaceMessage,
|
||||
isClosedIsolatedExecutionWorkspace,
|
||||
} from "./execution-workspace-guards.js";
|
||||
|
||||
export {
|
||||
instanceGeneralSettingsSchema,
|
||||
patchInstanceGeneralSettingsSchema,
|
||||
|
|
@ -594,14 +600,19 @@ export { deriveProjectUrlKey, normalizeProjectUrlKey, hasNonAsciiContent } from
|
|||
export {
|
||||
AGENT_MENTION_SCHEME,
|
||||
PROJECT_MENTION_SCHEME,
|
||||
SKILL_MENTION_SCHEME,
|
||||
buildAgentMentionHref,
|
||||
buildProjectMentionHref,
|
||||
buildSkillMentionHref,
|
||||
extractAgentMentionIds,
|
||||
extractSkillMentionIds,
|
||||
parseAgentMentionHref,
|
||||
parseProjectMentionHref,
|
||||
parseSkillMentionHref,
|
||||
extractProjectMentionIds,
|
||||
type ParsedAgentMention,
|
||||
type ParsedProjectMention,
|
||||
type ParsedSkillMention,
|
||||
} from "./project-mentions.js";
|
||||
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@ import { describe, expect, it } from "vitest";
|
|||
import {
|
||||
buildAgentMentionHref,
|
||||
buildProjectMentionHref,
|
||||
buildSkillMentionHref,
|
||||
extractAgentMentionIds,
|
||||
extractProjectMentionIds,
|
||||
extractSkillMentionIds,
|
||||
parseAgentMentionHref,
|
||||
parseProjectMentionHref,
|
||||
parseSkillMentionHref,
|
||||
} from "./project-mentions.js";
|
||||
|
||||
describe("project-mentions", () => {
|
||||
|
|
@ -26,4 +29,13 @@ describe("project-mentions", () => {
|
|||
});
|
||||
expect(extractAgentMentionIds(`[@CodexCoder](${href})`)).toEqual(["agent-123"]);
|
||||
});
|
||||
|
||||
it("round-trips skill mentions with slug metadata", () => {
|
||||
const href = buildSkillMentionHref("skill-123", "release-changelog");
|
||||
expect(parseSkillMentionHref(href)).toEqual({
|
||||
skillId: "skill-123",
|
||||
slug: "release-changelog",
|
||||
});
|
||||
expect(extractSkillMentionIds(`[/release-changelog](${href})`)).toEqual(["skill-123"]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export const PROJECT_MENTION_SCHEME = "project://";
|
||||
export const AGENT_MENTION_SCHEME = "agent://";
|
||||
export const SKILL_MENTION_SCHEME = "skill://";
|
||||
|
||||
const HEX_COLOR_RE = /^[0-9a-f]{6}$/i;
|
||||
const HEX_COLOR_SHORT_RE = /^[0-9a-f]{3}$/i;
|
||||
|
|
@ -7,7 +8,9 @@ const HEX_COLOR_WITH_HASH_RE = /^#[0-9a-f]{6}$/i;
|
|||
const HEX_COLOR_SHORT_WITH_HASH_RE = /^#[0-9a-f]{3}$/i;
|
||||
const PROJECT_MENTION_LINK_RE = /\[[^\]]*]\((project:\/\/[^)\s]+)\)/gi;
|
||||
const AGENT_MENTION_LINK_RE = /\[[^\]]*]\((agent:\/\/[^)\s]+)\)/gi;
|
||||
const SKILL_MENTION_LINK_RE = /\[[^\]]*]\((skill:\/\/[^)\s]+)\)/gi;
|
||||
const AGENT_ICON_NAME_RE = /^[a-z0-9-]+$/i;
|
||||
const SKILL_SLUG_RE = /^[a-z0-9][a-z0-9-]*$/i;
|
||||
|
||||
export interface ParsedProjectMention {
|
||||
projectId: string;
|
||||
|
|
@ -19,6 +22,11 @@ export interface ParsedAgentMention {
|
|||
icon: string | null;
|
||||
}
|
||||
|
||||
export interface ParsedSkillMention {
|
||||
skillId: string;
|
||||
slug: string | null;
|
||||
}
|
||||
|
||||
function normalizeHexColor(input: string | null | undefined): string | null {
|
||||
if (!input) return null;
|
||||
const trimmed = input.trim();
|
||||
|
|
@ -103,6 +111,36 @@ export function parseAgentMentionHref(href: string): ParsedAgentMention | null {
|
|||
};
|
||||
}
|
||||
|
||||
export function buildSkillMentionHref(skillId: string, slug?: string | null): string {
|
||||
const trimmedSkillId = skillId.trim();
|
||||
const normalizedSlug = normalizeSkillSlug(slug ?? null);
|
||||
if (!normalizedSlug) {
|
||||
return `${SKILL_MENTION_SCHEME}${trimmedSkillId}`;
|
||||
}
|
||||
return `${SKILL_MENTION_SCHEME}${trimmedSkillId}?s=${encodeURIComponent(normalizedSlug)}`;
|
||||
}
|
||||
|
||||
export function parseSkillMentionHref(href: string): ParsedSkillMention | null {
|
||||
if (!href.startsWith(SKILL_MENTION_SCHEME)) return null;
|
||||
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(href);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (url.protocol !== "skill:") return null;
|
||||
|
||||
const skillId = `${url.hostname}${url.pathname}`.replace(/^\/+/, "").trim();
|
||||
if (!skillId) return null;
|
||||
|
||||
return {
|
||||
skillId,
|
||||
slug: normalizeSkillSlug(url.searchParams.get("s") ?? url.searchParams.get("slug")),
|
||||
};
|
||||
}
|
||||
|
||||
export function extractProjectMentionIds(markdown: string): string[] {
|
||||
if (!markdown) return [];
|
||||
const ids = new Set<string>();
|
||||
|
|
@ -127,9 +165,28 @@ export function extractAgentMentionIds(markdown: string): string[] {
|
|||
return [...ids];
|
||||
}
|
||||
|
||||
export function extractSkillMentionIds(markdown: string): string[] {
|
||||
if (!markdown) return [];
|
||||
const ids = new Set<string>();
|
||||
const re = new RegExp(SKILL_MENTION_LINK_RE);
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = re.exec(markdown)) !== null) {
|
||||
const parsed = parseSkillMentionHref(match[1]);
|
||||
if (parsed) ids.add(parsed.skillId);
|
||||
}
|
||||
return [...ids];
|
||||
}
|
||||
|
||||
function normalizeAgentIcon(input: string | null | undefined): string | null {
|
||||
if (!input) return null;
|
||||
const trimmed = input.trim().toLowerCase();
|
||||
if (!trimmed || !AGENT_ICON_NAME_RE.test(trimmed)) return null;
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function normalizeSkillSlug(input: string | null | undefined): string | null {
|
||||
if (!input) return null;
|
||||
const trimmed = input.trim().toLowerCase();
|
||||
if (!trimmed || !SKILL_SLUG_RE.test(trimmed)) return null;
|
||||
return trimmed;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export class TelemetryClient {
|
|||
app,
|
||||
schemaVersion,
|
||||
installId: state.installId,
|
||||
version: this.version,
|
||||
events,
|
||||
}),
|
||||
signal: controller.signal,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,48 @@ export function trackCompanyImported(
|
|||
});
|
||||
}
|
||||
|
||||
export function trackProjectCreated(client: TelemetryClient): void {
|
||||
client.track("project.created");
|
||||
}
|
||||
|
||||
export function trackRoutineCreated(client: TelemetryClient): void {
|
||||
client.track("routine.created");
|
||||
}
|
||||
|
||||
export function trackRoutineRun(
|
||||
client: TelemetryClient,
|
||||
dims: { source: string; status: string },
|
||||
): void {
|
||||
client.track("routine.run", {
|
||||
source: dims.source,
|
||||
status: dims.status,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackGoalCreated(
|
||||
client: TelemetryClient,
|
||||
dims?: { goalLevel?: string | null },
|
||||
): void {
|
||||
client.track("goal.created", dims?.goalLevel ? { goal_level: dims.goalLevel } : undefined);
|
||||
}
|
||||
|
||||
export function trackAgentCreated(
|
||||
client: TelemetryClient,
|
||||
dims: { agentRole: string },
|
||||
): void {
|
||||
client.track("agent.created", { agent_role: dims.agentRole });
|
||||
}
|
||||
|
||||
export function trackSkillImported(
|
||||
client: TelemetryClient,
|
||||
dims: { sourceType: string; skillRef?: string | null },
|
||||
): void {
|
||||
client.track("skill.imported", {
|
||||
source_type: dims.sourceType,
|
||||
...(dims.skillRef ? { skill_ref: dims.skillRef } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
export function trackAgentFirstHeartbeat(
|
||||
client: TelemetryClient,
|
||||
dims: { agentRole: string },
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@ export {
|
|||
trackInstallStarted,
|
||||
trackInstallCompleted,
|
||||
trackCompanyImported,
|
||||
trackProjectCreated,
|
||||
trackRoutineCreated,
|
||||
trackRoutineRun,
|
||||
trackGoalCreated,
|
||||
trackAgentCreated,
|
||||
trackSkillImported,
|
||||
trackAgentFirstHeartbeat,
|
||||
trackAgentTaskCompleted,
|
||||
trackErrorHandlerCrash,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export interface TelemetryEventEnvelope {
|
|||
app: string;
|
||||
schemaVersion: string;
|
||||
installId: string;
|
||||
version: string;
|
||||
events: TelemetryEvent[];
|
||||
}
|
||||
|
||||
|
|
@ -31,6 +32,12 @@ export type TelemetryEventName =
|
|||
| "install.started"
|
||||
| "install.completed"
|
||||
| "company.imported"
|
||||
| "project.created"
|
||||
| "routine.created"
|
||||
| "routine.run"
|
||||
| "goal.created"
|
||||
| "agent.created"
|
||||
| "skill.imported"
|
||||
| "agent.first_heartbeat"
|
||||
| "agent.task_completed"
|
||||
| "error.handler_crash"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { z } from "zod";
|
||||
import {
|
||||
AGENT_ADAPTER_TYPES,
|
||||
INVITE_JOIN_TYPES,
|
||||
JOIN_REQUEST_STATUSES,
|
||||
JOIN_REQUEST_TYPES,
|
||||
PERMISSION_KEYS,
|
||||
} from "../constants.js";
|
||||
import { optionalAgentAdapterTypeSchema } from "../adapter-type.js";
|
||||
|
||||
export const createCompanyInviteSchema = z.object({
|
||||
allowedJoinTypes: z.enum(INVITE_JOIN_TYPES).default("both"),
|
||||
|
|
@ -26,7 +26,7 @@ export type CreateOpenClawInvitePrompt = z.infer<
|
|||
export const acceptInviteSchema = z.object({
|
||||
requestType: z.enum(JOIN_REQUEST_TYPES),
|
||||
agentName: z.string().min(1).max(120).optional(),
|
||||
adapterType: z.enum(AGENT_ADAPTER_TYPES).optional(),
|
||||
adapterType: optionalAgentAdapterTypeSchema,
|
||||
capabilities: z.string().max(4000).optional().nullable(),
|
||||
agentDefaultsPayload: z.record(z.string(), z.unknown()).optional().nullable(),
|
||||
// OpenClaw join compatibility fields accepted at top level.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { z } from "zod";
|
||||
import {
|
||||
AGENT_ADAPTER_TYPES,
|
||||
AGENT_ICON_NAMES,
|
||||
AGENT_ROLES,
|
||||
AGENT_STATUSES,
|
||||
INBOX_MINE_ISSUE_STATUS_FILTER,
|
||||
} from "../constants.js";
|
||||
import { agentAdapterTypeSchema } from "../adapter-type.js";
|
||||
import { envConfigSchema } from "./secret.js";
|
||||
|
||||
export const agentPermissionsSchema = z.object({
|
||||
|
|
@ -52,7 +52,7 @@ export const createAgentSchema = z.object({
|
|||
reportsTo: z.string().uuid().optional().nullable(),
|
||||
capabilities: z.string().optional().nullable(),
|
||||
desiredSkills: z.array(z.string().min(1)).optional(),
|
||||
adapterType: z.enum(AGENT_ADAPTER_TYPES).optional().default("process"),
|
||||
adapterType: agentAdapterTypeSchema,
|
||||
adapterConfig: adapterConfigSchema.optional().default({}),
|
||||
runtimeConfig: z.record(z.unknown()).optional().default({}),
|
||||
budgetMonthlyCents: z.number().int().nonnegative().optional().default(0),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue