paperclipai/paperclip · error · Error

typeof result.error === "string" ? result.error : `Project…

Error message

typeof result.error === "string" ? result.error : `Project tool failed (${response.status})`

What it means

callProjectTool() invokes Paperclip REST API endpoints (project/task tools) over authenticated HTTP. When the response status is not ok, it throws an Error whose message is the API's `error` string field if present, otherwise a generic `Project tool failed (<status>)`. This is a wrapper that surfaces server-side validation/permission/not-found failures from the underlying API call to the agent caller.

Solutions

  1. Read the thrown message: it carries the API's `error` field — fix the specific payload issue it names (missing/invalid projectId, title, validation constraints)
  2. Refresh the bearer token if the status was 401/403, and verify the agent key's company access
  3. Validate tool arguments against the tool input schema before calling callProjectTool
  4. For 5xx statuses, retry the call (it is idempotent-safe via idempotencyKey for create paths) after a short backoff
  5. Confirm apiUrl is the correct base (the code strips trailing slashes and a trailing /api) so the request reaches /api<path>

Example fix

// before: raw call surfaces generic failure
await callProjectTool({ name: "create_task", arguments: { projectId: "p_old" }, ... });
// throws: Project tool failed (404)
// after: validate references and handle API errors explicitly
const project = await callProjectTool({ name: "list_projects", arguments: {}, ... });
if (!project.projects.some((p) => p.id === args.projectId)) throw new Error(`Unknown projectId ${args.projectId}`);
try {
  return await callProjectTool({ name: "create_task", arguments: args, ... });
} catch (e) {
  if (String(e.message).includes("401")) { await refreshToken(); return retry(); }
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-validate arguments against the tool schema before calling the API
const parsed = tool.inputSchema.safeParse(args);
if (!parsed.success) throw new Error(`Invalid ${tool.name} arguments: ${parsed.error.message}`);
if (tool.name === "create_task" && args.projectId) {
  // verify the project exists/ is accessible first via list_projects
}

Type guard

function isProjectToolHttpError(e: unknown): e is Error & { status?: number } {
  return e instanceof Error && /^Project tool failed \(\d{3}\)$/.test(e.message) ||
    (e instanceof Error && typeof (e as { status?: number }).status === "number");
}

Try / catch

try {
  return await callProjectTool(input);
} catch (e) {
  const msg = e instanceof Error ? e.message : String(e);
  const status = Number(msg.match(/\((\d{3})\)$/)?.[1] ?? 0);
  if (status === 401 || status === 403) throw new Error(`Auth failure for project tool ${input.name}: refresh token / check company access`);
  if (status >= 500) return withBackoff(() => callProjectTool(input)); // transient, idempotencyKey makes retry safe
  throw e; // 4xx: surface the API's error message to the agent
}

Prevention

When it happens

Trigger: Any non-2xx response from the fetch to `/api/companies/{companyId}/projects`, `/project-repositories`, or `/issues`: 400 for invalid create_project/create_task payloads (zod parse passed but server validation failed), 401 for an expired/invalid bearer token, 403 for insufficient company permissions, 404 for a missing projectId, 409 conflicts, 422 validation errors, or 5xx server errors.

Common situations: Agent passing a nonexistent projectId to create_task; token expiring mid-conversation (401); agent exceeding project name/slug validation rules (400/422); the API URL being misconfigured so the request hits a wrong route (404); transient 500/503 from the API server.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/43d1f396b307ff54. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/project-tools.ts:50

    path = `/companies/${input.companyId}/issues`;
    body = createIssueSchema.parse({
      title: args.title, description: args.description, priority: args.priority,
      projectId: args.projectId, initialPlan: args.initialPlan,
      assigneeAgentId: args.assigneeActorId ?? input.agentId,
      parentId: input.conversation ? null : input.issueId,
      status: Array.isArray(args.blockedByTaskIds) && args.blockedByTaskIds.length ? "blocked" : "todo",
      blockedByIssueIds: args.blockedByTaskIds,
      idempotencyKey: `chat-handoff:${input.issueId}:${key}`,
    });
  } else throw badRequest("Unknown project tool");
  const response = await fetch(`${input.apiUrl.replace(/\/+$/, "").replace(/\/api$/, "")}/api${path}`, {
    method: body ? "POST" : "GET",
    headers: { Authorization: `Bearer ${input.token}`, "Content-Type": "application/json" },
    ...(body ? { body: JSON.stringify(body) } : {}),
    signal: AbortSignal.timeout(60_000),
  });
  const result = await response.json();
  if (!response.ok) throw new Error(typeof result.error === "string" ? result.error : `Project tool failed (${response.status})`);
  return input.name === "list_projects" ? { projects: result } : result;
}

View on GitHub (pinned to 3f1d897a7c)