Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/agent-worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ web path unless the request carries an explicit mobile or non-web runtime signal
high-confidence fallback materializes the project, scaffolds its canonical workspace, and
registers the managed preview even when the selected model would otherwise attempt generic shell
work and finish without a Computer target.
Template preparation records managed-preview ownership in checkpointed Workflow state. Every model
turn excludes the generic dev-server tool in that state, and every tool execution receives the same
request-scoped policy marker so stale tool calls cannot restart the server or replace root framework
manifests. Imported repositories use the separate unmanaged path and retain those capabilities.
The starter page is an internal server-readiness target, not user-facing generated content. A
fresh template run emits the typed `app-preview-status` transition from `building` to `ready` only
after model execution, so the web client can keep
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ interface RunAppBuilderOptions {

interface AppBuilderSetup {
agentContextNote?: string;
usesManagedPreview: boolean;
waitsForGeneratedPreview: boolean;
}

Expand All @@ -168,18 +169,21 @@ export async function runAppBuilder(options: RunAppBuilderOptions): Promise<AppB
if (await hasImportedAppWorkspace(sandbox, workspace.dir)) {
return {
...(await restoreImportedWorkspace(workspaceOptions)),
usesManagedPreview: false,
waitsForGeneratedPreview: false,
};
}
const shouldBootstrap = !(await hasExistingAppBuilderWorkspace(sandbox, workspace.dir));
if (shouldBootstrap && input.importRepoUrl) {
return {
...(await importRepoWorkspace({ ...workspaceOptions, repoUrl: input.importRepoUrl })),
usesManagedPreview: false,
waitsForGeneratedPreview: false,
};
}
return {
...(await runTemplateAppBuilder({ ...workspaceOptions, shouldBootstrap })),
usesManagedPreview: true,
waitsForGeneratedPreview: shouldBootstrap,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface MastraContextOptions {
sandbox: ProjectSandboxStub;
setRunStage: (stage: string) => Promise<void>;
workspaceResolver: WorkspaceResolver;
usesManagedPreview: boolean;
}

/** Resolves request-scoped credentials and user skills inside the active Workflow step. */
Expand Down Expand Up @@ -92,6 +93,7 @@ export function createAgentRequestContext(
return createCodeRequestContext(codeRuntime, {
agentDisplayName: input.agentDisplayName,
anthropicApiKey: credential.transportProvider === "anthropic" ? credential.apiKey : undefined,
appBuilderManagedPreview: options.usesManagedPreview,
composioApiKey: toolCredentials.composioApiKey,
composioConnectedAccounts: toolCredentials.composioConnectedAccounts,
composioQuotaMeter: toolCredentials.composioQuotaMeter,
Expand Down
3 changes: 2 additions & 1 deletion apps/agent-worker/src/durable-objects/agent-run-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type ProjectBoundAppBuilderRunOptions = AppBuilderRunOptions & {
export interface PreparedAppBuilderRun {
agentContextNote?: string;
options: ProjectBoundAppBuilderRunOptions;
usesManagedPreview: boolean;
waitsForGeneratedPreview: boolean;
}

Expand All @@ -44,7 +45,7 @@ export async function prepareAppBuilderRun(
const boundOptions = { ...options, input: requireProjectBinding(options.input) };
await warmSandbox(boundOptions.sandbox, boundOptions.logger);
if (boundOptions.isCanceled()) {
return { options: boundOptions, waitsForGeneratedPreview: false };
return { options: boundOptions, usesManagedPreview: false, waitsForGeneratedPreview: false };
}
const prepared = await runAppBuilder(boundOptions);
return { ...prepared, options: boundOptions };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const WorkflowToolCallSchema = z.strictObject({
});

export const WorkflowAgentStateSchema = z.strictObject({
appBuilderUsesManagedPreview: z.boolean(),
appBuilderWaitsForPreview: z.boolean(),
hasArtifact: z.boolean(),
hasVisibleText: z.boolean(),
Expand Down Expand Up @@ -138,6 +139,7 @@ export async function prepareWorkflowAgentRun(
const preparedApp = await prepareAppBuilderRun(runtime.pathOptions);
const threadContext = await loadWorkflowThreadContext(env, input, preparedApp?.agentContextNote);
return WorkflowAgentStateSchema.parse({
appBuilderUsesManagedPreview: preparedApp?.usesManagedPreview ?? false,
appBuilderWaitsForPreview: preparedApp?.waitsForGeneratedPreview ?? false,
hasArtifact: false,
hasVisibleText: false,
Expand Down Expand Up @@ -175,6 +177,7 @@ export async function generateWorkflowModelStep(
primary,
sandbox,
stub,
usesManagedPreview: state.appBuilderUsesManagedPreview,
});
} catch (error) {
if (!shouldFallbackToOpenAI(input.isModelExplicit, primary, false, error)) throw error;
Expand All @@ -190,6 +193,7 @@ export async function generateWorkflowModelStep(
primary: fallback,
sandbox,
stub,
usesManagedPreview: state.appBuilderUsesManagedPreview,
});
return {
...generated,
Expand Down Expand Up @@ -235,6 +239,7 @@ export async function executeWorkflowToolStep(
selectedLogicalModelId: state.selectedLogicalModelId,
stub,
toolCall,
usesManagedPreview: state.appBuilderUsesManagedPreview,
}),
run: input,
sandbox,
Expand Down Expand Up @@ -281,6 +286,7 @@ async function executePreparedWorkflowTool(input: {
selectedLogicalModelId: LogicalModelId | undefined;
stub: DurableObjectStub<AgentRun>;
toolCall: GeneralAgentToolCall;
usesManagedPreview: boolean;
}): Promise<WorkflowToolStepResult> {
const startedAt = Date.now();
const credential = await resolveCredentialForState(
Expand All @@ -297,6 +303,7 @@ async function executePreparedWorkflowTool(input: {
logger: input.logger,
sandbox: input.sandbox,
stub: input.stub,
usesManagedPreview: input.usesManagedPreview,
});
const prepared = await prepareMastraContext(
runtime.mastraOptions(credential),
Expand Down Expand Up @@ -394,6 +401,7 @@ async function generateWithCredential(input: {
primary: LlmCredential;
sandbox: ProjectSandboxStub;
stub: DurableObjectStub<AgentRun>;
usesManagedPreview: boolean;
}): Promise<WorkflowModelStepResult> {
const runtime = workflowRuntimeOptions({
callback: input.callback,
Expand All @@ -403,6 +411,7 @@ async function generateWithCredential(input: {
logger: input.logger,
sandbox: input.sandbox,
stub: input.stub,
usesManagedPreview: input.usesManagedPreview,
});
const options = runtime.mastraOptions(input.primary);
const prepared = await prepareMastraContext(options);
Expand All @@ -423,6 +432,7 @@ async function generateWithCredential(input: {
],
}
: {}),
...(input.usesManagedPreview ? { excludedTools: ["code_start_dev_server"] } : {}),
isDeepSeek: input.primary.transportProvider === "deepseek",
messages: input.messages,
requestContext,
Expand All @@ -443,6 +453,7 @@ function workflowRuntimeOptions(input: {
logger: ReturnType<typeof createLogger>;
sandbox: ProjectSandboxStub;
stub: DurableObjectStub<AgentRun>;
usesManagedPreview?: boolean;
}) {
const append = async (chunk: UIMessageChunk) => {
const event: AgentRunWorkflowEventInput = {
Expand Down Expand Up @@ -487,6 +498,7 @@ function workflowRuntimeOptions(input: {
logger: input.logger,
sandbox: input.sandbox,
setRunStage,
usesManagedPreview: input.usesManagedPreview ?? false,
workspaceResolver,
}),
};
Expand Down
15 changes: 10 additions & 5 deletions packages/agent-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ the returned post-action tree as the next actionable state. A click or fill ther
invoke a hidden model decision, expose a selector, reuse a stale ref, or cross the active origin.
Every first-party browser tool advertises its strict JSON schema to providers that support strict
tool calling, while the same Zod contract remains the provider-independent runtime boundary.
Action methods use explicit no-value, value, and drag shapes, so a model cannot omit a required ref
or value and still produce an executable call. A tool-validation or driver failure is verification
The model-facing action schema is a flat four-field object: method and ref are always present, while
unused value and targetRef fields are explicitly null. Runtime refinement maps that contract to the
driver's no-value, value, or drag shape, so operational timeout policy cannot become a substitute
for the action itself. A tool-validation or driver failure is verification
failure, not evidence that generated application state is broken; app-builder agents preserve the
framework event model and correct the browser call instead of injecting page scripts.
The managed
Expand All @@ -79,9 +81,12 @@ abort signal is forwarded through the Morph request. Focused and multi-section e
text file use FastApply; new files, binary files, and intentional whole-file rewrites continue to
use the deterministic file writer.
App-builder runs receive an existing framework workspace and managed preview before model
execution. Their shell tools reject app-initialization commands so a model cannot replace that
canonical root with another scaffold or a nested project; ordinary package installation and build
commands remain available.
execution. Managed template runs do not advertise the dev-server tool, and their shell and file
boundaries reject alternate server launches or replacement of root framework manifests. A model
therefore cannot replace the canonical Next.js or Expo root with another scaffold, package manifest,
or nested project; ordinary source edits, package installation, and build commands remain available.
Imported repositories keep their own dev-server and framework-file capabilities because their stack
is intentionally user-owned rather than image-owned.

## Code Checks

Expand Down
1 change: 1 addition & 0 deletions packages/agent-core/src/mastra/context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const CONTEXT = {
agentDisplayName: "agentDisplayName",
anthropicApiKey: "anthropicApiKey",
appBuilderManagedPreview: "appBuilderManagedPreview",
browserRunId: "browserRunId",
codeRuntime: "codeRuntime",
composioApiKey: "composioApiKey",
Expand Down
14 changes: 13 additions & 1 deletion packages/agent-core/src/mastra/durable-agent-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ interface GeneralAgentStepResult {
interface GenerateGeneralAgentStepOptions {
abortSignal?: AbortSignal;
activeTools?: string[];
excludedTools?: string[];
isDeepSeek: boolean;
messages: JSONValue[];
requestContext: RequestContext;
Expand All @@ -56,9 +57,10 @@ export async function generateGeneralAgentStep(
options: GenerateGeneralAgentStepOptions,
): Promise<GeneralAgentStepResult> {
const messages = options.messages.map((message) => modelMessageSchema.parse(message));
const activeTools = resolveActiveTools(options);
const result = await mastra.getAgent("generalStep").generate(messages as never, {
...(options.abortSignal ? { abortSignal: options.abortSignal } : {}),
...(options.activeTools ? { activeTools: options.activeTools } : {}),
...(activeTools ? { activeTools } : {}),
clientTools: cheatcodeTools,
...(options.isDeepSeek
? { providerOptions: { deepseek: { thinking: { type: "disabled" as const } } } }
Expand All @@ -81,6 +83,16 @@ export async function generateGeneralAgentStep(
};
}

function resolveActiveTools(options: GenerateGeneralAgentStepOptions): string[] | undefined {
if (options.activeTools && options.excludedTools) {
throw new Error("Agent tools cannot be included and excluded in the same model step.");
}
if (options.activeTools) return options.activeTools;
if (!options.excludedTools || options.excludedTools.length === 0) return undefined;
const excludedTools = new Set(options.excludedTools);
return Object.keys(cheatcodeTools).filter((toolName) => !excludedTools.has(toolName));
}

function toJsonValues(values: ModelMessage[]): JSONValue[] {
return values.map(toJsonValue);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core/src/mastra/system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ Build the Expo Router screens for a polished, native-feeling app: real screens,
// keeps WEB_MODULE's "start the dev server yourself" guidance; this note only applies here.
const APP_BUILDER_PREVIEW_NOTE = `## Your preview is already running — do not start your own

This project is scaffolded at the workspace root and its dev server + live preview are ALREADY running and managed for you before your turn begins (for a mobile app that's Metro serving the app on web plus the Expo Go QR). Do NOT initialize, scaffold, or create another app or nested project. Do NOT start, restart, or reconfigure the server yourself — no code_start_dev_server, \`expo start\`, \`npm run dev\`/\`web\`, or \`npx expo …\`: a second server fights the managed one for the project's port and breaks the preview. Use pnpm, never npm/npx, only when dependency changes are necessary. Inspect and edit the existing root files; the preview hot-reloads on save. Verify by opening the running app in the sandbox's headed Chromium at its INTERNAL localhost address; it's shown to the user automatically in the Computer/App panel — never paste the preview URL. Metro may briefly show an empty document while rebuilding the first web bundle after edits: wait for page content once and reload at most once before treating it as a defect. Take one screenshot with the exact visual acceptance criterion and use its returned PASS/FAIL assessment; never judge screenshot byte size. Exercise one representative interaction by calling browser_observe once, choosing one exact hyphenated element ref from its accessibility tree, and calling browser_act with that ref plus the required method/value. Use browser_act's actionable post-action tree as the result check; do not observe or extract again. If the request explicitly requires another interaction, chain it from a fresh ref in the returned tree; observe again only after navigation, an external page change, or when that tree lacks the required element. Never invent a ref or selector, write a separate Playwright/Python test, or install another browser. A browser-tool validation or driver error is a verification failure, not evidence of an app defect: correct the tool call once and preserve the framework's state and event model. Never replace React or React Native behavior with injected page scripts or manual DOM listeners to work around browser automation. If the rendered page or build output identifies a concrete code defect, fix that defect and repeat only the changed check once. Once the requested content renders, that interaction passes, and no blocking browser error remains, finish.`;
This project is scaffolded at the workspace root and its dev server + live preview are ALREADY running and managed for you before your turn begins (for a mobile app that's Metro serving the app on web plus the Expo Go QR). Preserve the existing Next.js or Expo framework: do NOT replace its package manifest, initialize or scaffold another framework, or create a nested project. Do NOT start, restart, or reconfigure the server yourself — no code_start_dev_server, \`expo start\`, \`npm run dev\`/\`web\`, or \`npx expo …\`: a second server fights the managed one for the project's port and breaks the preview. Use pnpm, never npm/npx, only when dependency changes are necessary. Inspect and edit the existing root files; the preview hot-reloads on save. Verify by opening the running app in the sandbox's headed Chromium at its INTERNAL localhost address; it's shown to the user automatically in the Computer/App panel — never paste the preview URL. Metro may briefly show an empty document while rebuilding the first web bundle after edits: wait for page content once and reload at most once before treating it as a defect. Take one screenshot with the exact visual acceptance criterion and use its returned PASS/FAIL assessment; never judge screenshot byte size. Exercise one representative interaction by calling browser_observe once, choosing one exact hyphenated element ref from its accessibility tree, and calling browser_act with that ref. Always pass browser_act all four fields: method, ref, targetRef, and value; use null for targetRef or value when the method does not use it. Use browser_act's actionable post-action tree as the result check; do not observe or extract again. If the request explicitly requires another interaction, chain it from a fresh ref in the returned tree; observe again only after navigation, an external page change, or when that tree lacks the required element. Never invent a ref or selector, write a separate Playwright/Python test, or install another browser. A browser-tool validation or driver error is a verification failure, not evidence of an app defect: correct the tool call once and preserve the framework's state and event model. Never replace React or React Native behavior with injected page scripts or manual DOM listeners to work around browser automation. If the rendered page or build output identifies a concrete code defect, fix that defect and repeat only the changed check once. Once the requested content renders, that interaction passes, and no blocking browser error remains, finish.`;

const DOCS_MODULE = `## Building documents & slides

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { APIError } from "@cheatcode/observability";
import { CONTEXT } from "../context";
import { requestContextFromToolContext } from "./tool-runtime-context";

const FRAMEWORK_MANIFESTS = new Set([
"app.json",
"babel.config.js",
"babel.config.mjs",
"metro.config.js",
"metro.config.mjs",
"next.config.js",
"next.config.mjs",
"next.config.ts",
"package-lock.json",
"package.json",
"pnpm-lock.yaml",
"postcss.config.js",
"postcss.config.mjs",
"tsconfig.json",
"yarn.lock",
]);

/** Keeps the managed Next.js or Expo runtime immutable while source files remain fully editable. */
export function assertManagedAppFrameworkFileMutable(context: unknown, path: string): void {
const requestContext = requestContextFromToolContext(context);
if (requestContext.get(CONTEXT.appBuilderManagedPreview) !== true) return;
if (!isProjectRootFrameworkManifest(requestContext.get(CONTEXT.promptWorkspaceDir), path)) return;
throw new APIError(
422,
"tool_validation_failed",
"The managed app framework manifest cannot be replaced or deleted.",
{
hint: "Edit application source files. Use pnpm add or pnpm remove when dependencies need to change.",
retriable: false,
},
);
}

function isProjectRootFrameworkManifest(workspaceDir: unknown, path: string): boolean {
const normalized = path.replace(/\/+$/u, "");
const filename = normalized.slice(normalized.lastIndexOf("/") + 1);
if (!FRAMEWORK_MANIFESTS.has(filename)) return false;
const parent = normalized.slice(0, normalized.lastIndexOf("/")) || "/";
return parent === "/workspace" || (typeof workspaceDir === "string" && parent === workspaceDir);
}
Loading