mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
Improve ACPX adapter configuration (#5290)
## Thinking Path > - Paperclip orchestrates AI agents across several adapter implementations. > - ACPX is a local adapter path that can proxy Claude and Codex-style execution. > - Its configuration needed stronger schema defaults, provider-aware model handling, and better UI support. > - Plugin authors also need clear docs for managed resources. > - This pull request improves ACPX adapter configuration and documents plugin-managed resources. > - The benefit is a more predictable adapter setup path without changing unrelated control-plane behavior. ## What Changed - Improved ACPX config schema, execution config handling, UI build config, and route coverage. - Added ACPX model filtering support and tests. - Updated the agent config form and storybook coverage for ACPX model/provider behavior. - Expanded plugin authoring documentation for managed resources. ## Verification - `pnpm install --frozen-lockfile` - `pnpm exec vitest run server/src/__tests__/acpx-local-execute.test.ts server/src/__tests__/adapter-routes.test.ts ui/src/lib/acpx-model-filter.test.ts` ## Risks - Low-to-medium risk: adapter configuration behavior changes can affect ACPX users, but the change is isolated to ACPX/plugin-doc surfaces and covered by targeted adapter tests. ## Model Used - OpenAI GPT-5 Codex via Paperclip `codex_local` adapter, with shell/git/GitHub CLI tool use. ## 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 --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
454edfe81e
commit
11ffd6f2c5
15 changed files with 949 additions and 211 deletions
|
|
@ -1,3 +1,5 @@
|
|||
import type { AdapterModel } from "@paperclipai/adapter-utils";
|
||||
|
||||
export const type = "acpx_local";
|
||||
export const label = "ACPX (local)";
|
||||
|
||||
|
|
@ -6,6 +8,7 @@ export const DEFAULT_ACPX_LOCAL_MODE = "persistent";
|
|||
export const DEFAULT_ACPX_LOCAL_PERMISSION_MODE = "approve-all";
|
||||
export const DEFAULT_ACPX_LOCAL_NON_INTERACTIVE_PERMISSIONS = "deny";
|
||||
export const DEFAULT_ACPX_LOCAL_TIMEOUT_SEC = 0;
|
||||
export const DEFAULT_ACPX_LOCAL_WARM_HANDLE_IDLE_MS = 0;
|
||||
|
||||
export const acpxAgentOptions = [
|
||||
{ id: "claude", label: "Claude via ACPX" },
|
||||
|
|
@ -13,6 +16,8 @@ export const acpxAgentOptions = [
|
|||
{ id: "custom", label: "Custom ACP command" },
|
||||
] as const;
|
||||
|
||||
export const models: AdapterModel[] = [];
|
||||
|
||||
export const agentConfigurationDoc = `# acpx_local agent configuration
|
||||
|
||||
Adapter: acpx_local
|
||||
|
|
@ -30,7 +35,7 @@ Don't use when:
|
|||
Core fields:
|
||||
- agent (string, optional): claude, codex, or custom. Defaults to claude.
|
||||
- agentCommand (string, optional): custom ACP command when agent=custom, or an override for a built-in ACP agent command.
|
||||
- mode (string, optional): persistent or oneshot. Defaults to persistent.
|
||||
- mode (string, optional): persistent or oneshot. Defaults to persistent. Paperclip keeps session state persistent and may close the live process between runs.
|
||||
- cwd (string, optional): default absolute working directory fallback for the agent process.
|
||||
- permissionMode (string, optional): defaults to approve-all, meaning ACPX permission requests are auto-approved.
|
||||
- nonInteractivePermissions (string, optional): fallback behavior when ACPX cannot ask interactively. Supported values are deny and fail.
|
||||
|
|
@ -38,7 +43,11 @@ Core fields:
|
|||
- instructionsFilePath (string, optional): absolute path to a markdown instructions file used by Paperclip prompt construction.
|
||||
- promptTemplate (string, optional): run prompt template.
|
||||
- bootstrapPromptTemplate (string, optional): first-run bootstrap prompt template.
|
||||
- model (string, optional): requested ACP model. Claude and Codex ACP agents both receive this through ACP session config.
|
||||
- effort/modelReasoningEffort (string, optional): requested thinking effort. Claude uses effort; Codex uses modelReasoningEffort/reasoning_effort.
|
||||
- fastMode (boolean, optional): for ACPX Codex, request Codex fast mode through ACP session config.
|
||||
- timeoutSec (number, optional): run timeout in seconds. Defaults to 0, meaning no adapter timeout.
|
||||
- warmHandleIdleMs (number, optional): live ACPX process idle window after a successful persistent run. Defaults to 0, meaning Paperclip shuts the process down after each run while retaining ACPX session state.
|
||||
- env (object, optional): KEY=VALUE environment variables or secret bindings.
|
||||
|
||||
Dependency decision:
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import type { AdapterConfigSchema } from "@paperclipai/adapter-utils";
|
||||
import {
|
||||
DEFAULT_ACPX_LOCAL_AGENT,
|
||||
DEFAULT_ACPX_LOCAL_MODE,
|
||||
DEFAULT_ACPX_LOCAL_NON_INTERACTIVE_PERMISSIONS,
|
||||
DEFAULT_ACPX_LOCAL_PERMISSION_MODE,
|
||||
DEFAULT_ACPX_LOCAL_TIMEOUT_SEC,
|
||||
DEFAULT_ACPX_LOCAL_WARM_HANDLE_IDLE_MS,
|
||||
acpxAgentOptions,
|
||||
} from "../index.js";
|
||||
|
||||
|
|
@ -26,27 +25,6 @@ export function getConfigSchema(): AdapterConfigSchema {
|
|||
type: "text",
|
||||
hint: "Required for custom agents; optional override for built-in Claude or Codex ACP commands.",
|
||||
},
|
||||
{
|
||||
key: "mode",
|
||||
label: "Session mode",
|
||||
type: "select",
|
||||
default: DEFAULT_ACPX_LOCAL_MODE,
|
||||
options: [
|
||||
{ value: "persistent", label: "Persistent" },
|
||||
{ value: "oneshot", label: "One shot" },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "permissionMode",
|
||||
label: "Permission mode",
|
||||
type: "select",
|
||||
default: DEFAULT_ACPX_LOCAL_PERMISSION_MODE,
|
||||
options: [
|
||||
{ value: "approve-all", label: "Approve all" },
|
||||
{ value: "default", label: "Approve reads" },
|
||||
],
|
||||
hint: "Defaults to maximum permissions. Approve reads grants read-only requests and asks for approval on writes.",
|
||||
},
|
||||
{
|
||||
key: "nonInteractivePermissions",
|
||||
label: "Non-interactive permissions",
|
||||
|
|
@ -56,6 +34,7 @@ export function getConfigSchema(): AdapterConfigSchema {
|
|||
{ value: "deny", label: "Deny" },
|
||||
{ value: "fail", label: "Fail" },
|
||||
],
|
||||
hint: "Fallback if the ACP agent asks for input outside an interactive session. Paperclip still auto-approves permissions by default.",
|
||||
},
|
||||
{
|
||||
key: "cwd",
|
||||
|
|
@ -70,20 +49,12 @@ export function getConfigSchema(): AdapterConfigSchema {
|
|||
hint: "Optional ACPX session state directory. Defaults to Paperclip-managed company/agent scoped storage.",
|
||||
},
|
||||
{
|
||||
key: "instructionsFilePath",
|
||||
label: "Instructions file",
|
||||
type: "text",
|
||||
hint: "Optional absolute path to markdown instructions injected into the run prompt.",
|
||||
},
|
||||
{
|
||||
key: "promptTemplate",
|
||||
label: "Prompt template",
|
||||
type: "textarea",
|
||||
},
|
||||
{
|
||||
key: "bootstrapPromptTemplate",
|
||||
label: "Bootstrap prompt template",
|
||||
type: "textarea",
|
||||
key: "fastMode",
|
||||
label: "Codex fast mode",
|
||||
type: "toggle",
|
||||
default: false,
|
||||
hint: "Only applies when ACP agent is Codex. Requests Codex Fast mode through ACP session config.",
|
||||
meta: { visibleWhen: { key: "agent", values: ["codex"] } },
|
||||
},
|
||||
{
|
||||
key: "timeoutSec",
|
||||
|
|
@ -91,6 +62,13 @@ export function getConfigSchema(): AdapterConfigSchema {
|
|||
type: "number",
|
||||
default: DEFAULT_ACPX_LOCAL_TIMEOUT_SEC,
|
||||
},
|
||||
{
|
||||
key: "warmHandleIdleMs",
|
||||
label: "Warm process idle ms",
|
||||
type: "number",
|
||||
default: DEFAULT_ACPX_LOCAL_WARM_HANDLE_IDLE_MS,
|
||||
hint: "Defaults to 0, which closes the ACPX process after each run while retaining persistent session state.",
|
||||
},
|
||||
{
|
||||
key: "env",
|
||||
label: "Environment JSON",
|
||||
|
|
|
|||
|
|
@ -45,10 +45,10 @@ import {
|
|||
DEFAULT_ACPX_LOCAL_NON_INTERACTIVE_PERMISSIONS,
|
||||
DEFAULT_ACPX_LOCAL_PERMISSION_MODE,
|
||||
DEFAULT_ACPX_LOCAL_TIMEOUT_SEC,
|
||||
DEFAULT_ACPX_LOCAL_WARM_HANDLE_IDLE_MS,
|
||||
} from "../index.js";
|
||||
|
||||
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const DEFAULT_WARM_HANDLE_IDLE_MS = 15 * 60 * 1000;
|
||||
const WRAPPER_CLEANUP_RETENTION_MS = 15 * 60 * 1000;
|
||||
const PAPERCLIP_MANAGED_CODEX_SKILLS_MANIFEST = ".paperclip-managed-skills.json";
|
||||
|
||||
|
|
@ -59,6 +59,7 @@ interface RuntimeCacheEntry {
|
|||
handle: AcpRuntimeHandle;
|
||||
fingerprint: string;
|
||||
lastUsedAt: number;
|
||||
cleanupTimer?: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
interface ExecuteDeps {
|
||||
|
|
@ -79,6 +80,9 @@ interface AcpxPreparedRuntime {
|
|||
stateDir: string;
|
||||
permissionMode: "approve-all" | "approve-reads" | "deny-all";
|
||||
nonInteractivePermissions: "deny" | "fail";
|
||||
requestedModel: string;
|
||||
requestedThinkingEffort: string;
|
||||
fastMode: boolean;
|
||||
timeoutSec: number;
|
||||
sessionKey: string;
|
||||
fingerprint: string;
|
||||
|
|
@ -504,6 +508,15 @@ function normalizeNonInteractivePermissions(config: Record<string, unknown>): "d
|
|||
: "deny";
|
||||
}
|
||||
|
||||
function normalizeRequestedThinkingEffort(config: Record<string, unknown>): string {
|
||||
return (
|
||||
asString(config.modelReasoningEffort, "") ||
|
||||
asString(config.reasoningEffort, "") ||
|
||||
asString(config.thinkingEffort, "") ||
|
||||
asString(config.effort, "")
|
||||
).trim();
|
||||
}
|
||||
|
||||
function isCompatibleSession(
|
||||
params: Record<string, unknown>,
|
||||
runtime: Pick<AcpxPreparedRuntime, "fingerprint" | "sessionKey" | "cwd" | "mode" | "acpxAgent" | "remoteExecutionIdentity">,
|
||||
|
|
@ -534,6 +547,9 @@ function buildSessionParams(input: {
|
|||
mode: prepared.mode,
|
||||
stateDir: prepared.stateDir,
|
||||
configFingerprint: prepared.fingerprint,
|
||||
...(prepared.requestedModel ? { model: prepared.requestedModel } : {}),
|
||||
...(prepared.requestedThinkingEffort ? { thinkingEffort: prepared.requestedThinkingEffort } : {}),
|
||||
...(prepared.fastMode ? { fastMode: true } : {}),
|
||||
skills: prepared.skillsIdentity,
|
||||
...(prepared.workspaceId ? { workspaceId: prepared.workspaceId } : {}),
|
||||
...(prepared.workspaceRepoUrl ? { repoUrl: prepared.workspaceRepoUrl } : {}),
|
||||
|
|
@ -644,6 +660,9 @@ async function buildRuntime(input: {
|
|||
const mode = normalizeMode(config);
|
||||
const permissionMode = normalizePermissionMode(config);
|
||||
const nonInteractivePermissions = normalizeNonInteractivePermissions(config);
|
||||
const requestedModel = asString(config.model, "").trim();
|
||||
const requestedThinkingEffort = normalizeRequestedThinkingEffort(config);
|
||||
const fastMode = acpxAgent === "codex" && config.fastMode === true;
|
||||
const timeoutSec = asNumber(config.timeoutSec, DEFAULT_ACPX_LOCAL_TIMEOUT_SEC);
|
||||
const stateDir = path.resolve(asString(config.stateDir, "") || defaultStateDir(agent.companyId, agent.id));
|
||||
await fs.mkdir(stateDir, { recursive: true });
|
||||
|
|
@ -741,6 +760,9 @@ async function buildRuntime(input: {
|
|||
mode,
|
||||
permissionMode,
|
||||
nonInteractivePermissions,
|
||||
requestedModel,
|
||||
requestedThinkingEffort,
|
||||
fastMode,
|
||||
remoteExecutionIdentity,
|
||||
skillsIdentity,
|
||||
skillPromptInstructions,
|
||||
|
|
@ -766,13 +788,16 @@ async function buildRuntime(input: {
|
|||
stateDir,
|
||||
permissionMode,
|
||||
nonInteractivePermissions,
|
||||
requestedModel,
|
||||
requestedThinkingEffort,
|
||||
fastMode,
|
||||
timeoutSec,
|
||||
sessionKey,
|
||||
fingerprint,
|
||||
agentCommand,
|
||||
agentRegistry,
|
||||
remoteExecutionIdentity,
|
||||
skillPromptInstructions,
|
||||
skillPromptInstructions,
|
||||
skillsIdentity: {
|
||||
...skillsIdentity,
|
||||
commandNotes: skillCommandNotes,
|
||||
|
|
@ -780,6 +805,51 @@ async function buildRuntime(input: {
|
|||
};
|
||||
}
|
||||
|
||||
function sessionConfigOptions(prepared: AcpxPreparedRuntime): Array<{ key: string; value: string }> {
|
||||
const options: Array<{ key: string; value: string }> = [];
|
||||
if (prepared.requestedModel) options.push({ key: "model", value: prepared.requestedModel });
|
||||
if (prepared.requestedThinkingEffort) {
|
||||
options.push({
|
||||
key: prepared.acpxAgent === "codex" ? "reasoning_effort" : "effort",
|
||||
value: prepared.requestedThinkingEffort,
|
||||
});
|
||||
}
|
||||
if (prepared.fastMode) {
|
||||
options.push(
|
||||
{ key: "service_tier", value: "fast" },
|
||||
{ key: "features.fast_mode", value: "true" },
|
||||
);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
async function applySessionConfigOptions(input: {
|
||||
runtime: AcpRuntime;
|
||||
handle: AcpRuntimeHandle;
|
||||
prepared: AcpxPreparedRuntime;
|
||||
onLog: AdapterExecutionContext["onLog"];
|
||||
}) {
|
||||
const options = sessionConfigOptions(input.prepared);
|
||||
if (options.length === 0) return;
|
||||
if (!input.runtime.setConfigOption) {
|
||||
const message =
|
||||
"ACPX runtime does not expose session config controls; upgrade ACPX or remove configured model, effort, and fast mode overrides.";
|
||||
await input.onLog("stderr", `[paperclip] ${message}\n`);
|
||||
throw new Error(message);
|
||||
}
|
||||
for (const option of options) {
|
||||
await input.runtime.setConfigOption({
|
||||
handle: input.handle,
|
||||
key: option.key,
|
||||
value: option.value,
|
||||
});
|
||||
await input.onLog(
|
||||
"stdout",
|
||||
`[paperclip] Applied ACPX ${input.prepared.acpxAgent} config ${option.key}=${option.value}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function buildPrompt(ctx: AdapterExecutionContext, resumedSession: boolean): Promise<{
|
||||
prompt: string;
|
||||
promptMetrics: Record<string, number>;
|
||||
|
|
@ -951,20 +1021,77 @@ async function cleanupIdleHandles(input: {
|
|||
now: number;
|
||||
idleMs: number;
|
||||
}) {
|
||||
if (input.idleMs <= 0) return;
|
||||
|
||||
const stale: Array<[string, RuntimeCacheEntry]> = [];
|
||||
for (const entry of input.handles.entries()) {
|
||||
if (input.now - entry[1].lastUsedAt >= input.idleMs) stale.push(entry);
|
||||
}
|
||||
for (const [key, entry] of stale) {
|
||||
input.handles.delete(key);
|
||||
await entry.runtime.close({
|
||||
handle: entry.handle,
|
||||
await closeWarmHandle({
|
||||
handles: input.handles,
|
||||
key,
|
||||
entry,
|
||||
reason: "paperclip idle cleanup",
|
||||
discardPersistentState: false,
|
||||
}).catch(() => {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function clearWarmHandleTimer(entry: RuntimeCacheEntry) {
|
||||
if (!entry.cleanupTimer) return;
|
||||
clearTimeout(entry.cleanupTimer);
|
||||
entry.cleanupTimer = undefined;
|
||||
}
|
||||
|
||||
async function closeWarmHandle(input: {
|
||||
handles: Map<string, RuntimeCacheEntry>;
|
||||
key: string;
|
||||
entry: RuntimeCacheEntry;
|
||||
reason: string;
|
||||
discardPersistentState?: boolean;
|
||||
}) {
|
||||
if (input.handles.get(input.key) === input.entry) {
|
||||
input.handles.delete(input.key);
|
||||
}
|
||||
clearWarmHandleTimer(input.entry);
|
||||
await input.entry.runtime.close({
|
||||
handle: input.entry.handle,
|
||||
reason: input.reason,
|
||||
discardPersistentState: input.discardPersistentState ?? false,
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function scheduleIdleHandleCleanup(input: {
|
||||
handles: Map<string, RuntimeCacheEntry>;
|
||||
key: string;
|
||||
entry: RuntimeCacheEntry;
|
||||
idleMs: number;
|
||||
now: () => number;
|
||||
}) {
|
||||
clearWarmHandleTimer(input.entry);
|
||||
if (input.idleMs <= 0) return;
|
||||
|
||||
const delayMs = Math.max(1, input.entry.lastUsedAt + input.idleMs - input.now());
|
||||
input.entry.cleanupTimer = setTimeout(() => {
|
||||
void (async () => {
|
||||
const current = input.handles.get(input.key);
|
||||
if (current !== input.entry) return;
|
||||
const idleForMs = input.now() - input.entry.lastUsedAt;
|
||||
if (idleForMs < input.idleMs) {
|
||||
scheduleIdleHandleCleanup(input);
|
||||
return;
|
||||
}
|
||||
await closeWarmHandle({
|
||||
handles: input.handles,
|
||||
key: input.key,
|
||||
entry: input.entry,
|
||||
reason: "paperclip idle cleanup",
|
||||
});
|
||||
})();
|
||||
}, delayMs);
|
||||
input.entry.cleanupTimer.unref?.();
|
||||
}
|
||||
|
||||
function warmHandleMatches(
|
||||
entry: RuntimeCacheEntry | undefined,
|
||||
runtime: AcpRuntime,
|
||||
|
|
@ -980,7 +1107,7 @@ export function createAcpxLocalExecutor(deps: ExecuteDeps = {}) {
|
|||
|
||||
return async function executeAcpxLocal(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
|
||||
const prepared = await buildRuntime({ ctx });
|
||||
const warmIdleMs = asNumber(ctx.config.warmHandleIdleMs, DEFAULT_WARM_HANDLE_IDLE_MS);
|
||||
const warmIdleMs = asNumber(ctx.config.warmHandleIdleMs, DEFAULT_ACPX_LOCAL_WARM_HANDLE_IDLE_MS);
|
||||
await cleanupIdleHandles({ handles: warmHandles, now: now(), idleMs: warmIdleMs });
|
||||
|
||||
const previousParams = parseObject(ctx.runtime.sessionParams);
|
||||
|
|
@ -996,6 +1123,7 @@ export function createAcpxLocalExecutor(deps: ExecuteDeps = {}) {
|
|||
timeoutMs: prepared.timeoutSec > 0 ? prepared.timeoutSec * 1000 : undefined,
|
||||
};
|
||||
const runtime = cached?.runtime ?? createRuntime(runtimeOptions);
|
||||
if (cached) clearWarmHandleTimer(cached);
|
||||
if (!canResume && asString(previousParams.runtimeSessionName, "")) {
|
||||
await ctx.onLog(
|
||||
"stdout",
|
||||
|
|
@ -1044,7 +1172,7 @@ export function createAcpxLocalExecutor(deps: ExecuteDeps = {}) {
|
|||
errorMessage: message,
|
||||
...classified,
|
||||
provider: "acpx",
|
||||
model: null,
|
||||
model: prepared.requestedModel || null,
|
||||
clearSession,
|
||||
resultJson: { phase: "ensure_session" },
|
||||
summary: message,
|
||||
|
|
@ -1059,12 +1187,52 @@ export function createAcpxLocalExecutor(deps: ExecuteDeps = {}) {
|
|||
errorMessage: "ACPX did not return a runtime session handle.",
|
||||
errorCode: "acpx_runtime_error",
|
||||
provider: "acpx",
|
||||
model: null,
|
||||
model: prepared.requestedModel || null,
|
||||
resultJson: { phase: "ensure_session" },
|
||||
summary: "ACPX did not return a runtime session handle.",
|
||||
};
|
||||
}
|
||||
const sessionHandle = handle;
|
||||
try {
|
||||
await applySessionConfigOptions({
|
||||
runtime,
|
||||
handle: sessionHandle,
|
||||
prepared,
|
||||
onLog: ctx.onLog,
|
||||
});
|
||||
} catch (err) {
|
||||
const classified = classifyError(err);
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
await emitAcpxLog(ctx, { type: "acpx.error", message, ...classified.errorMeta });
|
||||
await runtime.close({
|
||||
handle: sessionHandle,
|
||||
reason: "paperclip config cleanup",
|
||||
discardPersistentState: false,
|
||||
}).catch(() => {});
|
||||
const existing = warmHandles.get(prepared.sessionKey);
|
||||
if (warmHandleMatches(existing, runtime, sessionHandle) && existing) {
|
||||
clearWarmHandleTimer(existing);
|
||||
warmHandles.delete(prepared.sessionKey);
|
||||
}
|
||||
return {
|
||||
exitCode: 1,
|
||||
signal: null,
|
||||
timedOut: false,
|
||||
errorMessage: message,
|
||||
...classified,
|
||||
provider: "acpx",
|
||||
model: prepared.requestedModel || null,
|
||||
clearSession,
|
||||
resultJson: {
|
||||
phase: "configure_session",
|
||||
agent: prepared.acpxAgent,
|
||||
requestedModel: prepared.requestedModel || null,
|
||||
requestedThinkingEffort: prepared.requestedThinkingEffort || null,
|
||||
fastMode: prepared.fastMode,
|
||||
},
|
||||
summary: message,
|
||||
};
|
||||
}
|
||||
const { prompt, promptMetrics, commandNotes } = await buildPrompt(ctx, resumedSession);
|
||||
const runPrompt = joinPromptSections([prepared.skillPromptInstructions, prompt]);
|
||||
await emitAcpxLog(ctx, {
|
||||
|
|
@ -1076,6 +1244,9 @@ export function createAcpxLocalExecutor(deps: ExecuteDeps = {}) {
|
|||
runtimeSessionName: sessionHandle.runtimeSessionName,
|
||||
mode: prepared.mode,
|
||||
permissionMode: prepared.permissionMode,
|
||||
model: prepared.requestedModel || null,
|
||||
thinkingEffort: prepared.requestedThinkingEffort || null,
|
||||
fastMode: prepared.fastMode,
|
||||
});
|
||||
if (ctx.onMeta) {
|
||||
await ctx.onMeta({
|
||||
|
|
@ -1085,6 +1256,9 @@ export function createAcpxLocalExecutor(deps: ExecuteDeps = {}) {
|
|||
commandNotes: [
|
||||
`ACPX runtime embedded in Paperclip with ${prepared.mode} session mode.`,
|
||||
`Effective ACPX permission mode: ${prepared.permissionMode}.`,
|
||||
...(prepared.requestedModel ? [`Requested ACPX model: ${prepared.requestedModel}.`] : []),
|
||||
...(prepared.requestedThinkingEffort ? [`Requested ACPX thinking effort: ${prepared.requestedThinkingEffort}.`] : []),
|
||||
...(prepared.fastMode ? ["Requested ACPX Codex fast mode."] : []),
|
||||
...(Array.isArray(prepared.skillsIdentity.commandNotes)
|
||||
? prepared.skillsIdentity.commandNotes.filter((note): note is string => typeof note === "string")
|
||||
: []),
|
||||
|
|
@ -1130,15 +1304,23 @@ export function createAcpxLocalExecutor(deps: ExecuteDeps = {}) {
|
|||
const terminal = await turn.result;
|
||||
if (timeout) clearTimeout(timeout);
|
||||
if (terminal.status === "failed" || terminal.status === "cancelled" || timedOut) {
|
||||
if (warmHandleMatches(warmHandles.get(prepared.sessionKey), runtime, sessionHandle)) {
|
||||
warmHandles.delete(prepared.sessionKey);
|
||||
const existing = warmHandles.get(prepared.sessionKey);
|
||||
if (warmHandleMatches(existing, runtime, sessionHandle) && existing) {
|
||||
await closeWarmHandle({
|
||||
handles: warmHandles,
|
||||
key: prepared.sessionKey,
|
||||
entry: existing,
|
||||
reason: timedOut ? "paperclip timeout cleanup" : `paperclip turn ${terminal.status}`,
|
||||
discardPersistentState: terminal.status === "cancelled" || timedOut,
|
||||
});
|
||||
} else {
|
||||
await runtime.close({
|
||||
handle: sessionHandle,
|
||||
reason: timedOut ? "paperclip timeout cleanup" : `paperclip turn ${terminal.status}`,
|
||||
discardPersistentState: terminal.status === "cancelled" || timedOut,
|
||||
}).catch(() => {});
|
||||
}
|
||||
await runtime.close({
|
||||
handle: sessionHandle,
|
||||
reason: timedOut ? "paperclip timeout cleanup" : `paperclip turn ${terminal.status}`,
|
||||
discardPersistentState: terminal.status === "cancelled" || timedOut,
|
||||
}).catch(() => {});
|
||||
} else if (prepared.mode === "persistent") {
|
||||
} else if (prepared.mode === "persistent" && warmIdleMs > 0) {
|
||||
const existing = warmHandles.get(prepared.sessionKey);
|
||||
if (existing && !warmHandleMatches(existing, runtime, sessionHandle)) {
|
||||
await runtime.close({
|
||||
|
|
@ -1147,13 +1329,37 @@ export function createAcpxLocalExecutor(deps: ExecuteDeps = {}) {
|
|||
discardPersistentState: false,
|
||||
}).catch(() => {});
|
||||
} else {
|
||||
warmHandles.set(prepared.sessionKey, {
|
||||
const entry: RuntimeCacheEntry = {
|
||||
runtime,
|
||||
handle: sessionHandle,
|
||||
fingerprint: prepared.fingerprint,
|
||||
lastUsedAt: now(),
|
||||
};
|
||||
warmHandles.set(prepared.sessionKey, entry);
|
||||
scheduleIdleHandleCleanup({
|
||||
handles: warmHandles,
|
||||
key: prepared.sessionKey,
|
||||
entry,
|
||||
idleMs: warmIdleMs,
|
||||
now,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const existing = warmHandles.get(prepared.sessionKey);
|
||||
if (warmHandleMatches(existing, runtime, sessionHandle) && existing) {
|
||||
await closeWarmHandle({
|
||||
handles: warmHandles,
|
||||
key: prepared.sessionKey,
|
||||
entry: existing,
|
||||
reason: "paperclip completed turn cleanup",
|
||||
});
|
||||
} else {
|
||||
await runtime.close({
|
||||
handle: sessionHandle,
|
||||
reason: "paperclip completed turn cleanup",
|
||||
discardPersistentState: false,
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
const errorMessage = timedOut
|
||||
|
|
@ -1176,7 +1382,7 @@ export function createAcpxLocalExecutor(deps: ExecuteDeps = {}) {
|
|||
sessionParams: buildSessionParams({ prepared, handle: sessionHandle }),
|
||||
sessionDisplayId: sessionHandle.agentSessionId ?? sessionHandle.backendSessionId ?? sessionHandle.runtimeSessionName,
|
||||
provider: "acpx",
|
||||
model: null,
|
||||
model: prepared.requestedModel || null,
|
||||
billingType: "unknown",
|
||||
costUsd: null,
|
||||
resultJson: {
|
||||
|
|
@ -1184,6 +1390,9 @@ export function createAcpxLocalExecutor(deps: ExecuteDeps = {}) {
|
|||
stopReason: terminalStopReason,
|
||||
permissionMode: prepared.permissionMode,
|
||||
mode: prepared.mode,
|
||||
requestedModel: prepared.requestedModel || null,
|
||||
requestedThinkingEffort: prepared.requestedThinkingEffort || null,
|
||||
fastMode: prepared.fastMode,
|
||||
},
|
||||
summary: textParts.join("").trim() || terminalStopReason || terminal.status,
|
||||
clearSession,
|
||||
|
|
@ -1199,7 +1408,9 @@ export function createAcpxLocalExecutor(deps: ExecuteDeps = {}) {
|
|||
reason: timedOut ? "paperclip timeout cleanup" : "paperclip error cleanup",
|
||||
discardPersistentState: timedOut,
|
||||
}).catch(() => {});
|
||||
if (warmHandleMatches(warmHandles.get(prepared.sessionKey), runtime, sessionHandle)) {
|
||||
const existing = warmHandles.get(prepared.sessionKey);
|
||||
if (warmHandleMatches(existing, runtime, sessionHandle) && existing) {
|
||||
clearWarmHandleTimer(existing);
|
||||
warmHandles.delete(prepared.sessionKey);
|
||||
}
|
||||
await emitAcpxLog(ctx, { type: "acpx.error", message, ...classified.errorMeta });
|
||||
|
|
@ -1211,7 +1422,7 @@ export function createAcpxLocalExecutor(deps: ExecuteDeps = {}) {
|
|||
errorCode: timedOut ? "acpx_timeout" : classified.errorCode,
|
||||
errorMeta: classified.errorMeta,
|
||||
provider: "acpx",
|
||||
model: null,
|
||||
model: prepared.requestedModel || null,
|
||||
clearSession: clearSession || timedOut,
|
||||
resultJson: { phase: "turn" },
|
||||
summary: message,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
DEFAULT_ACPX_LOCAL_NON_INTERACTIVE_PERMISSIONS,
|
||||
DEFAULT_ACPX_LOCAL_PERMISSION_MODE,
|
||||
DEFAULT_ACPX_LOCAL_TIMEOUT_SEC,
|
||||
DEFAULT_ACPX_LOCAL_WARM_HANDLE_IDLE_MS,
|
||||
} from "../index.js";
|
||||
|
||||
function parseCommaArgs(value: string): string[] {
|
||||
|
|
@ -80,13 +81,15 @@ function readNumber(value: unknown, fallback: number): number {
|
|||
|
||||
export function buildAcpxLocalConfig(v: CreateConfigValues): Record<string, unknown> {
|
||||
const schemaValues = v.adapterSchemaValues ?? {};
|
||||
const agent = String(schemaValues.agent || DEFAULT_ACPX_LOCAL_AGENT);
|
||||
const ac: Record<string, unknown> = {
|
||||
agent: schemaValues.agent || DEFAULT_ACPX_LOCAL_AGENT,
|
||||
agent,
|
||||
mode: schemaValues.mode || DEFAULT_ACPX_LOCAL_MODE,
|
||||
permissionMode: schemaValues.permissionMode || DEFAULT_ACPX_LOCAL_PERMISSION_MODE,
|
||||
nonInteractivePermissions:
|
||||
schemaValues.nonInteractivePermissions || DEFAULT_ACPX_LOCAL_NON_INTERACTIVE_PERMISSIONS,
|
||||
timeoutSec: readNumber(schemaValues.timeoutSec, DEFAULT_ACPX_LOCAL_TIMEOUT_SEC),
|
||||
warmHandleIdleMs: readNumber(schemaValues.warmHandleIdleMs, DEFAULT_ACPX_LOCAL_WARM_HANDLE_IDLE_MS),
|
||||
};
|
||||
|
||||
for (const key of [
|
||||
|
|
@ -105,6 +108,11 @@ export function buildAcpxLocalConfig(v: CreateConfigValues): Record<string, unkn
|
|||
if (!ac.instructionsFilePath && v.instructionsFilePath) ac.instructionsFilePath = v.instructionsFilePath;
|
||||
if (!ac.promptTemplate && v.promptTemplate) ac.promptTemplate = v.promptTemplate;
|
||||
if (!ac.bootstrapPromptTemplate && v.bootstrapPrompt) ac.bootstrapPromptTemplate = v.bootstrapPrompt;
|
||||
if (v.model?.trim()) ac.model = v.model.trim();
|
||||
if (v.thinkingEffort) {
|
||||
ac[agent === "codex" ? "modelReasoningEffort" : "effort"] = v.thinkingEffort;
|
||||
}
|
||||
if (schemaValues.fastMode === true) ac.fastMode = true;
|
||||
|
||||
const env = parseEnvBindings(v.envBindings);
|
||||
const legacy = parseEnvVars(v.envVars);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue