mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 02:00:52 +01:00
Give agents persistent Ed25519 identities encrypted with the existing instance master key. Create keys transactionally for new agents and lazily before supported managed runs, expose public identities in the API and agent UI, and protect private material during runtime delivery and output persistence. Preserve identities in recovery backups while giving imported and development-cloned agents fresh keys. Co-Authored-By: Paperclip <noreply@paperclip.ing>
537 lines
19 KiB
TypeScript
537 lines
19 KiB
TypeScript
import { generateKeyPairSync, sign, verify } from "node:crypto";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { AdapterExecutionContext } from "@paperclipai/adapter-utils";
|
|
import { createPromptContextFixture } from "@paperclipai/adapter-utils/test-fixtures/prompt-context";
|
|
import { execute } from "./execute.js";
|
|
|
|
type MockRunOptions = {
|
|
id?: string;
|
|
agentId?: string;
|
|
status?: string;
|
|
waitResult?: Record<string, unknown>;
|
|
streamMessages?: unknown[];
|
|
streamError?: Error | null;
|
|
};
|
|
|
|
type MockAgentOptions = {
|
|
agentId?: string;
|
|
sendRun?: ReturnType<typeof createMockRun>;
|
|
};
|
|
|
|
const { createMock, resumeMock, getRunMock } = vi.hoisted(() => ({
|
|
createMock: vi.fn(),
|
|
resumeMock: vi.fn(),
|
|
getRunMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@cursor/sdk", () => ({
|
|
Agent: {
|
|
create: createMock,
|
|
resume: resumeMock,
|
|
getRun: getRunMock,
|
|
},
|
|
}));
|
|
|
|
function createMockRun(options: MockRunOptions = {}) {
|
|
const runId = options.id ?? "run-123";
|
|
const agentId = options.agentId ?? "agent-123";
|
|
const status = options.status ?? "finished";
|
|
const waitResult = options.waitResult ?? {
|
|
id: runId,
|
|
status,
|
|
result: "Done\nWith detail",
|
|
model: { id: "gpt-5.4" },
|
|
durationMs: 1234,
|
|
};
|
|
const streamMessages = options.streamMessages ?? [];
|
|
const streamError = options.streamError ?? null;
|
|
|
|
return {
|
|
id: runId,
|
|
agentId,
|
|
status,
|
|
result: typeof waitResult.result === "string" ? waitResult.result : null,
|
|
model: waitResult.model ?? null,
|
|
durationMs: waitResult.durationMs ?? null,
|
|
git: waitResult.git ?? null,
|
|
supports(capability: string) {
|
|
return capability === "stream" || capability === "wait";
|
|
},
|
|
async *stream() {
|
|
for (const message of streamMessages) yield message;
|
|
if (streamError) throw streamError;
|
|
},
|
|
async wait() {
|
|
return waitResult;
|
|
},
|
|
};
|
|
}
|
|
|
|
function createMockSdkAgent(options: MockAgentOptions = {}) {
|
|
const sendRun = options.sendRun ?? createMockRun();
|
|
return {
|
|
agentId: options.agentId ?? sendRun.agentId,
|
|
send: vi.fn(async (_prompt: string, _options?: Record<string, unknown>) => sendRun),
|
|
[Symbol.asyncDispose]: vi.fn(async () => {}),
|
|
};
|
|
}
|
|
|
|
function createContext(
|
|
overrides: Partial<AdapterExecutionContext> = {},
|
|
): AdapterExecutionContext & {
|
|
logs: Array<{ stream: "stdout" | "stderr"; chunk: string }>;
|
|
meta: Record<string, unknown>[];
|
|
} {
|
|
const logs: Array<{ stream: "stdout" | "stderr"; chunk: string }> = [];
|
|
const meta: Record<string, unknown>[] = [];
|
|
const agent = overrides.agent ?? {
|
|
id: "agent-1",
|
|
companyId: "company-1",
|
|
name: "Cursor Cloud Agent",
|
|
adapterType: "cursor_cloud",
|
|
adapterConfig: {},
|
|
};
|
|
const runtime = overrides.runtime ?? {
|
|
sessionId: null,
|
|
sessionParams: null,
|
|
sessionDisplayId: null,
|
|
taskKey: null,
|
|
};
|
|
const config = overrides.config ?? {
|
|
env: {
|
|
CURSOR_API_KEY: "cursor-secret",
|
|
EXTRA_FLAG: "1",
|
|
},
|
|
repoUrl: "https://github.com/paperclipai/paperclip.git",
|
|
repoStartingRef: "main",
|
|
runtimeEnvType: "cloud",
|
|
promptTemplate: "Do the work for {{agent.name}}",
|
|
model: "gpt-5.4",
|
|
};
|
|
const context = overrides.context ?? {
|
|
taskId: "issue-1",
|
|
issueId: "issue-1",
|
|
wakeReason: "issue_commented",
|
|
};
|
|
|
|
const base: AdapterExecutionContext = {
|
|
runId: "run-heartbeat-1",
|
|
agent,
|
|
runtime,
|
|
config,
|
|
context,
|
|
authToken: "paperclip-run-jwt",
|
|
onLog: async (stream, chunk) => {
|
|
logs.push({ stream, chunk });
|
|
},
|
|
onMeta: async (entry) => {
|
|
meta.push(entry as unknown as Record<string, unknown>);
|
|
},
|
|
};
|
|
|
|
return {
|
|
...base,
|
|
...overrides,
|
|
logs,
|
|
meta,
|
|
};
|
|
}
|
|
|
|
describe("cursor_cloud execute", () => {
|
|
beforeEach(() => {
|
|
createMock.mockReset();
|
|
resumeMock.mockReset();
|
|
getRunMock.mockReset();
|
|
});
|
|
|
|
it.each([false, true])("sends the central chat directive to Cursor Cloud (custom=%s)", async (custom) => {
|
|
const sdkAgent = createMockSdkAgent();
|
|
createMock.mockResolvedValue(sdkAgent);
|
|
const ctx = createContext();
|
|
if (!custom) delete ctx.config.promptTemplate;
|
|
const directive = "Chat directive: clarify goals and hand plans off to project tasks.";
|
|
ctx.context = {
|
|
...ctx.context,
|
|
conversationMode: true,
|
|
paperclipTaskMarkdown: directive,
|
|
paperclipWake: {
|
|
reason: "issue_commented",
|
|
issue: { id: "issue-1", workMode: "planning", status: "in_progress" },
|
|
interactionKind: "request_confirmation",
|
|
interactionStatus: "accepted",
|
|
},
|
|
};
|
|
const result = await execute(ctx);
|
|
expect(result.exitCode).toBe(0);
|
|
const prompt = String(sdkAgent.send.mock.calls[0]?.[0]);
|
|
expect(prompt).toContain(directive);
|
|
expect(prompt).toContain(custom ? "Do the work for" : "You are agent agent-1");
|
|
expect(prompt).not.toContain("Execution contract:");
|
|
expect(prompt).not.toContain("Create child issues");
|
|
});
|
|
|
|
it("sends assignment context on an ordinary cloud task turn", async () => {
|
|
const sdkAgent = createMockSdkAgent();
|
|
createMock.mockResolvedValue(sdkAgent);
|
|
const ctx = createContext({ context: createPromptContextFixture() });
|
|
delete ctx.config.promptTemplate;
|
|
const result = await execute(ctx);
|
|
expect(result.exitCode).toBe(0);
|
|
const prompt = String(sdkAgent.send.mock.calls[0]?.[0]);
|
|
expect(prompt).toContain("## Owned assignment");
|
|
expect(prompt).toContain("You are agent agent-1 (Cursor Cloud Agent).");
|
|
expect(prompt).toContain("Connection tools:");
|
|
expect(prompt).not.toContain("Execution contract:");
|
|
expect(prompt.indexOf("Append the same ledger entry.")).toBeLessThan(prompt.indexOf("Change the final scope to the launch checklist."));
|
|
expect(prompt.split("Append the same ledger entry.")).toHaveLength(3);
|
|
});
|
|
|
|
it("delivers a large wake through the SDK prompt without a configured JSON env copy", async () => {
|
|
const sdkAgent = createMockSdkAgent();
|
|
createMock.mockResolvedValue(sdkAgent);
|
|
const ctx = createContext();
|
|
const description = "start " + "context ".repeat(25_000) + " end";
|
|
ctx.config.env = { CURSOR_API_KEY: "cursor-secret", PAPERCLIP_WAKE_PAYLOAD_JSON: description };
|
|
ctx.context.paperclipWake = {
|
|
reason: "issue_assigned",
|
|
issue: { id: "issue-1", description },
|
|
};
|
|
const result = await execute(ctx);
|
|
expect(result.exitCode).toBe(0);
|
|
expect(createMock.mock.calls[0]?.[0]?.cloud?.envVars).not.toHaveProperty("PAPERCLIP_WAKE_PAYLOAD_JSON");
|
|
expect(sdkAgent.send.mock.calls[0]?.[0]).toContain(description);
|
|
});
|
|
|
|
it("creates a fresh Cursor agent and injects Paperclip env without CURSOR_API_KEY", async () => {
|
|
const run = createMockRun({
|
|
agentId: "agent-fresh",
|
|
streamMessages: [
|
|
{
|
|
type: "assistant",
|
|
message: {
|
|
content: [{ type: "text", text: "Working" }],
|
|
},
|
|
},
|
|
],
|
|
});
|
|
const sdkAgent = createMockSdkAgent({ agentId: "agent-fresh", sendRun: run });
|
|
createMock.mockResolvedValue(sdkAgent);
|
|
const ctx = createContext();
|
|
|
|
const result = await execute(ctx);
|
|
|
|
expect(createMock).toHaveBeenCalledTimes(1);
|
|
expect(resumeMock).not.toHaveBeenCalled();
|
|
expect(getRunMock).not.toHaveBeenCalled();
|
|
expect(createMock.mock.calls[0]?.[0]).toMatchObject({
|
|
apiKey: "cursor-secret",
|
|
name: "Paperclip Cursor Cloud Agent",
|
|
model: { id: "gpt-5.4" },
|
|
cloud: {
|
|
env: { type: "cloud" },
|
|
repos: [{ url: "https://github.com/paperclipai/paperclip.git", startingRef: "main" }],
|
|
},
|
|
});
|
|
expect(createMock.mock.calls[0]?.[0]?.cloud?.envVars).toMatchObject({
|
|
EXTRA_FLAG: "1",
|
|
PAPERCLIP_RUN_ID: "run-heartbeat-1",
|
|
PAPERCLIP_TASK_ID: "issue-1",
|
|
PAPERCLIP_WAKE_REASON: "issue_commented",
|
|
PAPERCLIP_API_KEY: "paperclip-run-jwt",
|
|
});
|
|
// When a run JWT is present the callback URL is retained so the worker can
|
|
// authenticate its Paperclip API calls.
|
|
expect(createMock.mock.calls[0]?.[0]?.cloud?.envVars).toHaveProperty("PAPERCLIP_API_URL");
|
|
expect(createMock.mock.calls[0]?.[0]?.cloud?.envVars).not.toHaveProperty("CURSOR_API_KEY");
|
|
|
|
expect(result).toMatchObject({
|
|
exitCode: 0,
|
|
errorMessage: null,
|
|
sessionId: "agent-fresh",
|
|
model: "gpt-5.4",
|
|
summary: "Done",
|
|
sessionParams: {
|
|
cursorAgentId: "agent-fresh",
|
|
latestRunId: "run-123",
|
|
runtime: "cloud",
|
|
envType: "cloud",
|
|
repos: [{ url: "https://github.com/paperclipai/paperclip.git", startingRef: "main" }],
|
|
},
|
|
});
|
|
expect(ctx.logs.map((entry) => entry.chunk)).toEqual(
|
|
expect.arrayContaining([
|
|
expect.stringContaining('"type":"cursor_cloud.init"'),
|
|
expect.stringContaining('"type":"cursor_cloud.message"'),
|
|
expect.stringContaining('"type":"cursor_cloud.result"'),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("delivers the assigned identity to the managed cloud environment without exposing it in prompts or metadata", async () => {
|
|
const keys = generateKeyPairSync("ed25519");
|
|
const identity = {
|
|
keyId: "sha256:assigned-identity",
|
|
publicKeyPem: keys.publicKey.export({ type: "spki", format: "pem" }).toString(),
|
|
privateKeyPem: keys.privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
|
|
};
|
|
const sdkAgent = createMockSdkAgent();
|
|
createMock.mockResolvedValue(sdkAgent);
|
|
const ctx = createContext({ agentIdentity: identity });
|
|
ctx.config.env = {
|
|
CURSOR_API_KEY: "cursor-secret",
|
|
PAPERCLIP_AGENT_KEY_ID: "configured-override",
|
|
PAPERCLIP_AGENT_PRIVATE_KEY: "configured-override",
|
|
paperclip_agent_private_key: "configured-override",
|
|
};
|
|
|
|
await execute(ctx);
|
|
|
|
const env = createMock.mock.calls[0]?.[0]?.cloud?.envVars;
|
|
expect(env).toMatchObject({
|
|
PAPERCLIP_AGENT_KEY_ID: identity.keyId,
|
|
PAPERCLIP_AGENT_PUBLIC_KEY: identity.publicKeyPem,
|
|
PAPERCLIP_AGENT_PRIVATE_KEY: identity.privateKeyPem,
|
|
});
|
|
expect(env).not.toHaveProperty("paperclip_agent_private_key");
|
|
const challenge = Buffer.from("managed cloud identity");
|
|
expect(verify(null, challenge, identity.publicKeyPem,
|
|
sign(null, challenge, env.PAPERCLIP_AGENT_PRIVATE_KEY))).toBe(true);
|
|
const privateBody = identity.privateKeyPem.split("\n")[1];
|
|
expect(JSON.stringify([ctx.meta, ctx.logs, sdkAgent.send.mock.calls])).not.toContain(privateBody);
|
|
});
|
|
|
|
it("omits empty environment values while preserving nonempty values exactly", async () => {
|
|
createMock.mockResolvedValue(createMockSdkAgent());
|
|
const ctx = createContext();
|
|
ctx.config.env = {
|
|
CURSOR_API_KEY: "cursor-secret",
|
|
GH_TOKEN: "",
|
|
GITHUB_TOKEN: "",
|
|
GIT_AUTHOR_NAME: "",
|
|
SSH_AUTH_SOCK: "",
|
|
EMPTY_PLAIN: { type: "plain", value: "" },
|
|
EXTRA_FLAG: "0",
|
|
PADDED_VALUE: " retain whitespace ",
|
|
};
|
|
|
|
await execute(ctx);
|
|
|
|
const env = createMock.mock.calls[0]?.[0]?.cloud?.envVars;
|
|
expect(env).not.toHaveProperty("GH_TOKEN");
|
|
expect(env).not.toHaveProperty("GITHUB_TOKEN");
|
|
expect(env).not.toHaveProperty("GIT_AUTHOR_NAME");
|
|
expect(env).not.toHaveProperty("SSH_AUTH_SOCK");
|
|
expect(env).not.toHaveProperty("EMPTY_PLAIN");
|
|
expect(env).not.toHaveProperty("CURSOR_API_KEY");
|
|
expect(env).toMatchObject({ EXTRA_FLAG: "0", PADDED_VALUE: " retain whitespace " });
|
|
expect(Object.values(env).every((value) => value !== "")).toBe(true);
|
|
});
|
|
|
|
it("reports dispatch before starting the first remote SDK operation", async () => {
|
|
const run = createMockRun({ agentId: "agent-dispatch" });
|
|
const sdkAgent = createMockSdkAgent({ agentId: "agent-dispatch", sendRun: run });
|
|
createMock.mockResolvedValue(sdkAgent);
|
|
const onDispatch = vi.fn();
|
|
|
|
await execute(createContext({ onDispatch }));
|
|
|
|
expect(onDispatch).toHaveBeenCalledTimes(1);
|
|
expect(onDispatch.mock.invocationCallOrder[0]).toBeLessThan(createMock.mock.invocationCallOrder[0]!);
|
|
});
|
|
|
|
it("omits the Paperclip API callback when no run JWT is issued (remote worker cannot call home)", async () => {
|
|
const run = createMockRun({ agentId: "agent-no-jwt" });
|
|
const sdkAgent = createMockSdkAgent({ agentId: "agent-no-jwt", sendRun: run });
|
|
createMock.mockResolvedValue(sdkAgent);
|
|
// cursor_cloud is registered with supportsLocalAgentJwt=false, so heartbeat
|
|
// passes no authToken. A remote cloud worker must not receive a callback URL
|
|
// it can neither reach nor authenticate against (the source of 401 noise).
|
|
const ctx = createContext({ authToken: undefined });
|
|
|
|
await execute(ctx);
|
|
|
|
const envVars = (createMock.mock.calls[0]?.[0]?.cloud?.envVars ?? {}) as Record<string, string>;
|
|
expect(envVars).not.toHaveProperty("PAPERCLIP_API_KEY");
|
|
expect(envVars).not.toHaveProperty("PAPERCLIP_API_URL");
|
|
expect(envVars).not.toHaveProperty("PAPERCLIP_API_BRIDGE_MODE");
|
|
// Informational Paperclip env (non-credential) still flows through.
|
|
expect(envVars).toMatchObject({
|
|
PAPERCLIP_RUN_ID: "run-heartbeat-1",
|
|
PAPERCLIP_AGENT_ID: "agent-1",
|
|
PAPERCLIP_COMPANY_ID: "company-1",
|
|
});
|
|
});
|
|
|
|
it("resumes a matching saved session when no active run can be reattached", async () => {
|
|
getRunMock.mockResolvedValue(createMockRun({ status: "finished" }));
|
|
const resumedRun = createMockRun({ id: "run-resumed", agentId: "agent-resumed" });
|
|
const sdkAgent = createMockSdkAgent({ agentId: "agent-resumed", sendRun: resumedRun });
|
|
resumeMock.mockResolvedValue(sdkAgent);
|
|
const ctx = createContext({
|
|
runtime: {
|
|
sessionId: null,
|
|
sessionDisplayId: "agent-previous",
|
|
taskKey: null,
|
|
sessionParams: {
|
|
cursorAgentId: "agent-previous",
|
|
latestRunId: "run-previous",
|
|
runtime: "cloud",
|
|
envType: "cloud",
|
|
repos: [{ url: "https://github.com/paperclipai/paperclip.git", startingRef: "main" }],
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await execute(ctx);
|
|
|
|
expect(getRunMock).toHaveBeenCalledWith("run-previous", {
|
|
runtime: "cloud",
|
|
agentId: "agent-previous",
|
|
apiKey: "cursor-secret",
|
|
});
|
|
expect(resumeMock).toHaveBeenCalledTimes(1);
|
|
expect(createMock).not.toHaveBeenCalled();
|
|
expect(sdkAgent.send).toHaveBeenCalledTimes(1);
|
|
expect(result.sessionId).toBe("agent-resumed");
|
|
});
|
|
|
|
it("reattaches to an active run, drains it, then sends the heartbeat as a follow-up", async () => {
|
|
const attachedRun = createMockRun({
|
|
id: "run-attached",
|
|
agentId: "agent-attached",
|
|
status: "running",
|
|
waitResult: {
|
|
id: "run-attached",
|
|
status: "finished",
|
|
result: "Prior result",
|
|
model: { id: "gpt-5.4" },
|
|
},
|
|
streamMessages: [
|
|
{
|
|
type: "status",
|
|
status: "running",
|
|
message: "Still working",
|
|
},
|
|
],
|
|
});
|
|
getRunMock.mockResolvedValue(attachedRun);
|
|
const followUpRun = createMockRun({
|
|
id: "run-followup",
|
|
agentId: "agent-attached",
|
|
waitResult: {
|
|
id: "run-followup",
|
|
status: "finished",
|
|
result: "Follow-up result",
|
|
model: { id: "gpt-5.4" },
|
|
},
|
|
});
|
|
const sdkAgent = createMockSdkAgent({ agentId: "agent-attached", sendRun: followUpRun });
|
|
resumeMock.mockResolvedValue(sdkAgent);
|
|
const ctx = createContext({
|
|
runtime: {
|
|
sessionId: null,
|
|
sessionDisplayId: "agent-attached",
|
|
taskKey: null,
|
|
sessionParams: {
|
|
cursorAgentId: "agent-attached",
|
|
latestRunId: "run-attached",
|
|
runtime: "cloud",
|
|
envType: "cloud",
|
|
repos: [{ url: "https://github.com/paperclipai/paperclip.git", startingRef: "main" }],
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await execute(ctx);
|
|
|
|
expect(getRunMock).toHaveBeenCalledTimes(1);
|
|
expect(createMock).not.toHaveBeenCalled();
|
|
expect(resumeMock).toHaveBeenCalledTimes(1);
|
|
expect(sdkAgent.send).toHaveBeenCalledTimes(1);
|
|
expect(result).toMatchObject({
|
|
exitCode: 0,
|
|
sessionId: "agent-attached",
|
|
summary: "Follow-up result",
|
|
resultJson: {
|
|
cursorRunId: "run-followup",
|
|
},
|
|
});
|
|
const logChunks = ctx.logs.map((entry) => entry.chunk);
|
|
expect(logChunks).toEqual(
|
|
expect.arrayContaining([
|
|
expect.stringContaining("Reattached to existing Cursor run run-attached."),
|
|
expect.stringContaining("Prior Cursor run run-attached finished"),
|
|
expect.stringContaining("Started Cursor run run-followup."),
|
|
expect.stringContaining('"runId":"run-attached"'),
|
|
expect.stringContaining('"runId":"run-followup"'),
|
|
]),
|
|
);
|
|
expect(ctx.meta[0]?.context).toMatchObject({
|
|
cursorCloud: {
|
|
canReuseSession: true,
|
|
repoUrl: "https://github.com/paperclipai/paperclip.git",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("explains a rejected Cursor default without silently choosing another model", async () => {
|
|
const sdkAgent = createMockSdkAgent();
|
|
sdkAgent.send.mockRejectedValue(new Error("[invalid_model] Model 'gpt-5' is not available or invalid."));
|
|
createMock.mockResolvedValue(sdkAgent);
|
|
const ctx = createContext();
|
|
delete ctx.config.model;
|
|
|
|
const result = await execute(ctx);
|
|
|
|
expect(createMock.mock.calls[0]?.[0]).not.toHaveProperty("model");
|
|
expect(sdkAgent.send).toHaveBeenCalledWith(expect.any(String), {});
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.errorMessage).toContain("Cursor rejected its configured default model");
|
|
expect(result.errorMessage).toContain("https://cursor.com/dashboard/cloud-agents");
|
|
expect(sdkAgent.send).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("makes Cursor repository setup failures actionable without launching another run", async () => {
|
|
const sdkAgent = createMockSdkAgent();
|
|
sdkAgent.send.mockRejectedValue(new Error("[validation_error] Failed to determine repository default branch"));
|
|
createMock.mockResolvedValue(sdkAgent);
|
|
|
|
const result = await execute(createContext());
|
|
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.errorMessage).toContain("Cursor's GitHub integration can access https://github.com/paperclipai/paperclip.git");
|
|
expect(result.errorMessage).toContain("https://cursor.com/dashboard/cloud-agents");
|
|
expect(sdkAgent.send).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("maps non-finished Cursor results to failing Paperclip runs", async () => {
|
|
const cancelledRun = createMockRun({
|
|
id: "run-cancelled",
|
|
agentId: "agent-cancelled",
|
|
status: "cancelled",
|
|
waitResult: {
|
|
id: "run-cancelled",
|
|
status: "cancelled",
|
|
result: "",
|
|
model: { id: "gpt-5.4" },
|
|
},
|
|
});
|
|
const sdkAgent = createMockSdkAgent({ agentId: "agent-cancelled", sendRun: cancelledRun });
|
|
createMock.mockResolvedValue(sdkAgent);
|
|
const ctx = createContext();
|
|
|
|
const result = await execute(ctx);
|
|
|
|
expect(result).toMatchObject({
|
|
exitCode: 1,
|
|
errorMessage: "Cursor run cancelled",
|
|
sessionId: "agent-cancelled",
|
|
resultJson: {
|
|
status: "cancelled",
|
|
cursorAgentId: "agent-cancelled",
|
|
cursorRunId: "run-cancelled",
|
|
},
|
|
});
|
|
});
|
|
});
|