Add Meta Muse Spark computer-use provider#59
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Integration screenshot path needs cwd
- I replaced the single cwd-based path with a resolver that finds
examples/screenshot.pngfrom eitherpackages/aior monorepo-root test runs.
- I replaced the single cwd-based path with a resolver that finds
- ✅ Fixed: Meta tests ignore MODEL_API_KEY
- I added a shared Meta API key helper using
META_MODEL_API_KEY ?? MODEL_API_KEYand used it for both gating and request auth in Meta integration tests.
- I added a shared Meta API key helper using
Or push these changes by commenting:
@cursor push 4874c204f2
Preview (4874c204f2)
diff --git a/packages/ai/test/computer-tool.integration.test.ts b/packages/ai/test/computer-tool.integration.test.ts
--- a/packages/ai/test/computer-tool.integration.test.ts
+++ b/packages/ai/test/computer-tool.integration.test.ts
@@ -1,5 +1,6 @@
+import { existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
-import { join } from "node:path";
+import { dirname, join } from "node:path";
import { describe, expect, it } from "vitest";
import {
CUA_ACTION_TYPES,
@@ -16,8 +17,30 @@
yutori,
} from "../src/index";
-const screenshotPath = join(process.cwd(), "examples", "screenshot.png");
+function resolveScreenshotPath(): string {
+ let currentDir = process.cwd();
+ while (true) {
+ const localPath = join(currentDir, "examples", "screenshot.png");
+ if (existsSync(localPath)) {
+ return localPath;
+ }
+
+ const monorepoPath = join(currentDir, "packages", "ai", "examples", "screenshot.png");
+ if (existsSync(monorepoPath)) {
+ return monorepoPath;
+ }
+
+ const parentDir = dirname(currentDir);
+ if (parentDir === currentDir) {
+ return monorepoPath;
+ }
+ currentDir = parentDir;
+ }
+}
+
+const screenshotPath = resolveScreenshotPath();
+
interface ProviderCase {
provider: CuaProvider;
envVar: string;
@@ -26,9 +49,13 @@
coordinateRange: readonly [number, number];
requireToolCalls: boolean;
ciOptInEnvVar?: string;
+ getApiKey?: () => string | undefined;
extraOptions?: Record<string, unknown>;
}
+const getMetaApiKey = (): string | undefined =>
+ process.env.META_MODEL_API_KEY ?? process.env.MODEL_API_KEY;
+
const cases: ProviderCase[] = [
{
provider: "openai",
@@ -62,6 +89,7 @@
tools: () => meta.computerTools({ actions: ["click"] }),
coordinateRange: [0, 1000],
requireToolCalls: true,
+ getApiKey: getMetaApiKey,
},
{
provider: "tzafon",
@@ -114,7 +142,8 @@
describe("individual computer action integration", () => {
for (const c of cases) {
- const hasKey = !!process.env[c.envVar];
+ const apiKey = c.getApiKey?.() ?? process.env[c.envVar];
+ const hasKey = !!apiKey;
const ciEnabled = !c.ciOptInEnvVar || !process.env.CI || process.env[c.ciOptInEnvVar] === "1";
const test = hasKey ? it : it.skip;
@@ -122,7 +151,7 @@
const model = getCuaModel(c.modelRef as never);
const context = await buildContext(c.tools);
const response = await cuaModels().complete(model, context, {
- apiKey: process.env[c.envVar],
+ apiKey,
maxTokens: 1024,
...c.extraOptions,
});
@@ -149,7 +178,8 @@
}, 60_000);
}
- const metaHasKey = !!process.env.META_MODEL_API_KEY;
+ const metaApiKey = getMetaApiKey();
+ const metaHasKey = !!metaApiKey;
(metaHasKey ? it : it.skip)(
"meta continues a screenshot tool loop with previous_response_id",
async () => {
@@ -171,7 +201,7 @@
tools,
};
const first = await cuaModels().complete(model, context, {
- apiKey: process.env.META_MODEL_API_KEY,
+ apiKey: metaApiKey,
maxTokens: 1024,
});
const click = first.content.find((part) => part.type === "toolCall" && part.name === "click");
@@ -191,7 +221,7 @@
let payload: Record<string, unknown> | undefined;
const second = await cuaModels().complete(model, context, {
- apiKey: process.env.META_MODEL_API_KEY,
+ apiKey: metaApiKey,
maxTokens: 1024,
onPayload: (value) => {
payload = value as Record<string, unknown>;You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: AGENTS loader omits MODEL_API_KEY
- Updated the AGENTS.md key-loading loop to include MODEL_API_KEY so Meta credentials load when only the official alias is set.
- ✅ Fixed: Missing TSDoc on buildMetaSystemPrompt
- Added a TSDoc block describing buildMetaSystemPrompt purpose and the optional suffix behavior to satisfy exported API documentation rules.
Or push these changes by commenting:
@cursor push 1144551363
Preview (1144551363)
diff --git a/.agents/skills/update-models/SKILL.md b/.agents/skills/update-models/SKILL.md
--- a/.agents/skills/update-models/SKILL.md
+++ b/.agents/skills/update-models/SKILL.md
@@ -16,7 +16,7 @@
eval "$(python3 - <<'PY'
import pathlib, re, shlex
text = pathlib.Path('~/AGENTS.md').expanduser().read_text()
-for key in ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GOOGLE_API_KEY', 'META_MODEL_API_KEY', 'TZAFON_API_KEY', 'YUTORI_API_KEY']:
+for key in ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GOOGLE_API_KEY', 'META_MODEL_API_KEY', 'MODEL_API_KEY', 'TZAFON_API_KEY', 'YUTORI_API_KEY']:
m = re.search(r'export\s+' + re.escape(key) + r'=(?:"([^"]+)"|([^\s\n]+))', text)
if m:
print(f'export {key}={shlex.quote(m.group(1) or m.group(2))}')
diff --git a/packages/ai/src/providers/meta/index.ts b/packages/ai/src/providers/meta/index.ts
--- a/packages/ai/src/providers/meta/index.ts
+++ b/packages/ai/src/providers/meta/index.ts
@@ -29,6 +29,12 @@
export const META_COMPUTER_INSTRUCTIONS = `You control a Kernel cloud browser through individual browser tools. Coordinates are normalized from 0 to 1000 relative to the most recent screenshot, with (0, 0) at the top left and (1000, 1000) at the bottom right. Base each action on the latest observed state and request a screenshot when you need a fresh view.`;
+/**
+ * Builds Meta's default system prompt for computer-use runs.
+ *
+ * The optional suffix lets callers append run-specific instructions while
+ * preserving the shared base guidance for tool usage and coordinates.
+ */
export function buildMetaSystemPrompt(opts: { suffix?: string } = {}): string {
return [META_COMPUTER_INSTRUCTIONS, opts.suffix].filter(Boolean).join("\n\n");
}You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Meta smoke screenshot path
- Updated
smokeMetato resolvescreenshot.pngfrom either package-local or repo-root paths before reading it, preventing cwd-dependent failures.
- Updated
Or push these changes by commenting:
@cursor push bf7648b5f2
Preview (bf7648b5f2)
diff --git a/.agents/skills/update-models/reference/discover-models.ts b/.agents/skills/update-models/reference/discover-models.ts
--- a/.agents/skills/update-models/reference/discover-models.ts
+++ b/.agents/skills/update-models/reference/discover-models.ts
@@ -1,4 +1,5 @@
#!/usr/bin/env tsx
+import { existsSync } from "node:fs";
import { readFile, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
@@ -201,7 +202,12 @@
async function smokeMeta(client: any, model: string): Promise<SmokeResult> {
try {
- const screenshot = await readFile(join(process.cwd(), "packages", "ai", "examples", "screenshot.png"));
+ const screenshotPath = [
+ join(process.cwd(), "examples", "screenshot.png"),
+ join(process.cwd(), "packages", "ai", "examples", "screenshot.png"),
+ ].find(existsSync);
+ if (!screenshotPath) throw new Error("could not find packages/ai/examples/screenshot.png");
+ const screenshot = await readFile(screenshotPath);
const response = await client.responses.create({
model,
store: false,You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Missing Meta API key aliases
- Meta credential resolution now accepts META_API_KEY, META_MODEL_API_KEY, and MODEL_API_KEY in both runtime API-key helpers and the Meta model discovery smoke path, with tests covering fallback order.
Or push these changes by commenting:
@cursor push eb0ea9e80c
Preview (eb0ea9e80c)
diff --git a/.agents/skills/update-models/reference/discover-models.ts b/.agents/skills/update-models/reference/discover-models.ts
--- a/.agents/skills/update-models/reference/discover-models.ts
+++ b/.agents/skills/update-models/reference/discover-models.ts
@@ -179,7 +179,7 @@
async function discoverMeta(args: Args): Promise<Record<string, unknown>> {
const OpenAI = await importDefault("openai", "OpenAI");
- const apiKey = process.env.META_API_KEY;
+ const apiKey = process.env.META_API_KEY || process.env.META_MODEL_API_KEY || process.env.MODEL_API_KEY;
const client = new OpenAI({ apiKey, baseURL: "https://api.meta.ai/v1" });
const rawModels = await collectAsync(client.models.list());
const models: ModelResult[] = rawModels.map((m) => ({
diff --git a/packages/ai/src/api-keys.ts b/packages/ai/src/api-keys.ts
--- a/packages/ai/src/api-keys.ts
+++ b/packages/ai/src/api-keys.ts
@@ -12,7 +12,7 @@
openai: ["OPENAI_API_KEY"],
anthropic: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
google: ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
- meta: ["META_API_KEY"],
+ meta: ["META_API_KEY", "META_MODEL_API_KEY", "MODEL_API_KEY"],
tzafon: ["TZAFON_API_KEY"],
yutori: ["YUTORI_API_KEY"],
};
diff --git a/packages/ai/test/api-keys.test.ts b/packages/ai/test/api-keys.test.ts
--- a/packages/ai/test/api-keys.test.ts
+++ b/packages/ai/test/api-keys.test.ts
@@ -13,6 +13,8 @@
"GOOGLE_API_KEY",
"GEMINI_API_KEY",
"META_API_KEY",
+ "META_MODEL_API_KEY",
+ "MODEL_API_KEY",
"TZAFON_API_KEY",
"YUTORI_API_KEY",
] as const;
@@ -32,7 +34,7 @@
expect(cuaApiKeyEnvVarsForProvider("openai")).toEqual(["OPENAI_API_KEY"]);
expect(cuaApiKeyEnvVarsForProvider("google")).toEqual(["GOOGLE_API_KEY", "GEMINI_API_KEY"]);
expect(cuaApiKeyEnvVarsForProvider("gemini")).toEqual(["GOOGLE_API_KEY", "GEMINI_API_KEY"]);
- expect(cuaApiKeyEnvVarsForProvider("meta")).toEqual(["META_API_KEY"]);
+ expect(cuaApiKeyEnvVarsForProvider("meta")).toEqual(["META_API_KEY", "META_MODEL_API_KEY", "MODEL_API_KEY"]);
expect(cuaApiKeyEnvVarsForProvider("unknown")).toEqual([]);
});
@@ -42,6 +44,15 @@
expect(getCuaEnvApiKey("google")).toBe("gemini");
process.env.GOOGLE_API_KEY = "google";
expect(getCuaEnvApiKey("google")).toBe("google");
+
+ delete process.env.META_API_KEY;
+ delete process.env.META_MODEL_API_KEY;
+ process.env.MODEL_API_KEY = "model";
+ expect(getCuaEnvApiKey("meta")).toBe("model");
+ process.env.META_MODEL_API_KEY = "meta-model";
+ expect(getCuaEnvApiKey("meta")).toBe("meta-model");
+ process.env.META_API_KEY = "meta";
+ expect(getCuaEnvApiKey("meta")).toBe("meta");
});
it("resolves keys from model refs", () => {You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Include strip before payload hooks
- Meta payload sanitization now removes include after caller onPayload hooks run, and a regression test covers hooks that reintroduce it during threaded requests.
Or push these changes by commenting:
@cursor push 0c3d56c5f2
Preview (0c3d56c5f2)
diff --git a/packages/ai/src/providers/meta/provider.ts b/packages/ai/src/providers/meta/provider.ts
--- a/packages/ai/src/providers/meta/provider.ts
+++ b/packages/ai/src/providers/meta/provider.ts
@@ -39,7 +39,10 @@
};
// CUA uses stored response state instead of stateless encrypted reasoning replay.
delete next.include;
- return options?.onPayload ? ((await options.onPayload(next, model)) ?? next) : next;
+ const prepared = options?.onPayload ? ((await options.onPayload(next, model)) ?? next) : next;
+ const sanitized = { ...(prepared as Record<string, unknown>) };
+ delete sanitized.include;
+ return sanitized;
};
return { context: messages === context.messages ? context : { ...context, messages }, onPayload };
}
diff --git a/packages/ai/test/meta-threading.test.ts b/packages/ai/test/meta-threading.test.ts
--- a/packages/ai/test/meta-threading.test.ts
+++ b/packages/ai/test/meta-threading.test.ts
@@ -75,4 +75,15 @@
},
});
});
+
+ it("strips include after caller payload hooks run", async () => {
+ const { onPayload } = threadMetaRequest(multiTurnContext(), {
+ onPayload: (payload) => ({ ...payload, include: ["reasoning.encrypted_content"] }),
+ });
+ expect(await onPayload({}, model)).toEqual({
+ store: true,
+ parallel_tool_calls: false,
+ previous_response_id: "resp_meta_1",
+ });
+ });
});You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Harness onPayload skips Meta sanitization
- CuaAgentHarness now re-sanitizes Meta payloads after its onPayload hook by stripping include fields, and a regression test covers this path.
Or push these changes by commenting:
@cursor push bfd6b303c0
Preview (bfd6b303c0)
diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts
--- a/packages/agent/src/agent.ts
+++ b/packages/agent/src/agent.ts
@@ -714,8 +714,14 @@
}
this.on("before_provider_payload", async ({ model, payload }: { model: Model<Api>; payload: unknown }) => {
const onPayload = this.runtime.onPayload();
- if (!onPayload) return { payload };
- return { payload: (await onPayload(payload, model)) ?? payload };
+ const nextPayload = onPayload ? ((await onPayload(payload, model)) ?? payload) : payload;
+ if (model.provider !== "meta" || !nextPayload || typeof nextPayload !== "object") {
+ return { payload: nextPayload };
+ }
+ const sanitized = { ...(nextPayload as Record<string, unknown>) };
+ // Keep Meta requests aligned with threadMetaRequest: include is unsupported.
+ delete sanitized.include;
+ return { payload: sanitized };
});
}
diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts
--- a/packages/agent/test/agent.test.ts
+++ b/packages/agent/test/agent.test.ts
@@ -1466,6 +1466,51 @@
expect(providerImageCount).toBe(4);
});
+ it("removes include from meta payloads after harness onPayload hooks", async () => {
+ const services = await createHarnessServices();
+ let observedPayload: unknown;
+ const streamFn: StreamFn = (model, _context, options) => {
+ const stream = createAssistantMessageEventStream();
+ void (async () => {
+ observedPayload = await options?.onPayload?.(
+ {
+ previous_response_id: "resp_meta_1",
+ include: ["reasoning.encrypted_content"],
+ },
+ model,
+ );
+ const message = finishMessage(createAssistantMessage(model), "done");
+ stream.push({ type: "start", partial: message });
+ stream.push({ type: "done", reason: "stop", message });
+ stream.end(message);
+ })();
+ return stream;
+ };
+ const models = createCuaModels();
+ models.setProvider({
+ id: "meta",
+ name: "meta test provider",
+ auth: { apiKey: { name: "test key", resolve: async () => ({ auth: { apiKey: "test-key" } }) } },
+ getModels: () => [],
+ stream: streamFn,
+ streamSimple: streamFn,
+ });
+ const harness = new CuaAgentHarness({
+ ...services,
+ browser,
+ client,
+ model: "meta:muse-spark-1.1",
+ models,
+ onPayload: (payload) => ({ ...(payload as object), include: ["reasoning.encrypted_content"] }),
+ });
+
+ await harness.prompt("next");
+
+ expect(observedPayload).toEqual({
+ previous_response_id: "resp_meta_1",
+ });
+ });
+
it("applies image projection after user context hooks", async () => {
const run = async (replace: boolean) => {
const services = await createHarnessServices();You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Meta hooks may re-enable parallel tools
- I updated Meta payload sanitization to force any emitted
parallel_tool_callsfield back tofalseafter hooks run and added a regression test covering a hook that sets it totrue.
- I updated Meta payload sanitization to force any emitted
Or push these changes by commenting:
@cursor push c662100f7c
Preview (c662100f7c)
diff --git a/packages/ai/src/providers/meta/provider.ts b/packages/ai/src/providers/meta/provider.ts
--- a/packages/ai/src/providers/meta/provider.ts
+++ b/packages/ai/src/providers/meta/provider.ts
@@ -34,6 +34,7 @@
const sanitized = { ...(prepared as Record<string, unknown>) };
// CUA uses stored response state instead of stateless encrypted reasoning replay.
delete sanitized.include;
+ if ("parallel_tool_calls" in sanitized) sanitized.parallel_tool_calls = false;
return sanitized;
};
return { context: threaded.context, onPayload };
diff --git a/packages/ai/test/meta-threading.test.ts b/packages/ai/test/meta-threading.test.ts
--- a/packages/ai/test/meta-threading.test.ts
+++ b/packages/ai/test/meta-threading.test.ts
@@ -86,4 +86,15 @@
previous_response_id: "resp_meta_1",
});
});
+
+ it("keeps parallel tool calls disabled after caller payload hooks", async () => {
+ const { onPayload } = threadMetaRequest(multiTurnContext(), {
+ onPayload: (payload) => ({ ...(payload as object), parallel_tool_calls: true }),
+ });
+ expect(await onPayload({}, model)).toEqual({
+ store: true,
+ parallel_tool_calls: false,
+ previous_response_id: "resp_meta_1",
+ });
+ });
});You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Stale threading TSDoc omits Meta
- Updated
CuaSimpleStreamOptionsTSDoc to state thatdisableResponseThreadingalso applies to Meta requests.
- Updated
Or push these changes by commenting:
@cursor push 862a910071
Preview (862a910071)
diff --git a/packages/ai/src/providers/common.ts b/packages/ai/src/providers/common.ts
--- a/packages/ai/src/providers/common.ts
+++ b/packages/ai/src/providers/common.ts
@@ -272,7 +272,7 @@
* pi-ai `SimpleStreamOptions` plus CUA request controls consumed by provider
* adapters. Pass `keepToolNames` for caller tools that must survive native
* tool-set substitution; use `disableResponseThreading` for a full-context
- * OpenAI or Tzafon request.
+ * OpenAI, Meta, or Tzafon request.
*/
export interface CuaSimpleStreamOptions extends SimpleStreamOptions, ResponseThreadingOptions {
keepToolNames?: readonly string[];You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit c4e92c5. Configure here.
35c4981 to
6bcb1c5
Compare


Summary
meta:muse-spark-1.1with Meta Model API authentication and OpenAI-compatible Responses streamingprevious_response_id, keep GUI actions serial, and remove encrypted reasoning replay where Meta rejects itTesting
npm run typechecknpm test --workspace @onkernel/cua-ai(149 tests)npm test --workspace @onkernel/cua-agent(160 tests; live matrix skipped in the unit run)npm test --workspace @onkernel/cua-cli(88 tests; TUI fixtures skipped)previous_response_idCuaAgentandCuaAgentHarnessbrowser smokesexample.comnpm run buildcompleted the AI, agent, TypeScript, and CLI build steps; the unrelated ptywright native step could not run because Zig is unavailable locally.Note
Medium Risk
Touches provider routing and multi-turn Responses threading (shared with OpenAI), which can affect tool-loop behavior; scope is a new provider path with substantial test coverage rather than changes to existing provider defaults.
Overview
Adds Meta as a first-class CUA provider for
meta:muse-spark-1.1(Muse Spark 1.1), using the OpenAI-compatible Responses API athttps://api.meta.ai/v1withMETA_API_KEYinstead of a nativecomputertool.The new
meta-responsesstream adapter reuses pi-ai’s Responses transport but applies Meta-specific request rules:store: true,previous_response_idthreading for multi-turn tool loops,parallel_tool_calls: false, and strippinginclude: reasoning.encrypted_content(incompatible with threaded requests). OpenAI’s threading logic is factored into sharedthreadResponsesRequestincommon.ts; Meta wraps it withthreadMetaRequest.Runtime wiring registers Meta in the model catalog (
CUA_MODEL_ANNOTATIONS,CUA_MODEL_OVERRIDES), routes models toMETA_RESPONSES_API, and exposes canonical computer tools with a 0–1000 normalized coordinate system (computer mode only). AgentresponseThreadingdocs and behavior now include Meta alongside OpenAI and Tzafon.Docs, CLI (
cua models -p meta, env vars), update-models discovery/smoke/drift scripts, and CI integration/e2e jobs gain Meta coverage; tests cover threading payloads and live screenshot loops when keys are present.Reviewed by Cursor Bugbot for commit 6bcb1c5. Bugbot is set up for automated code reviews on this repo. Configure here.