From 9ac55d6650da29462e5ee000fcb7de74dccfde83 Mon Sep 17 00:00:00 2001 From: iamjr15 Date: Thu, 13 Aug 2026 04:25:27 +0530 Subject: [PATCH] fix(agent): preserve structured composer intent Carry work intent, selected skills, and connected apps as validated run metadata. Preserve exact user text and make surface choices authoritative before keyword routing. --- apps/agent-worker/README.md | 5 +- apps/agent-worker/src/agent-routing.ts | 2 + .../agent-run-mastra-context.ts | 2 + .../src/durable-objects/agent-run-schemas.ts | 10 ++- apps/web/README.md | 13 +++- .../components/chat/chat-panel-controller.ts | 15 +++- .../components/chat/chat-panel-submission.ts | 18 ++++- .../web/src/components/chat/chat-transport.ts | 2 + .../chat/prompt-composer-controller.ts | 19 +++-- .../components/chat/use-chat-submission.ts | 52 +++++++++---- .../composer/composer-context-chips.tsx | 20 ----- .../home/home-composer-controller.ts | 3 + .../home/home-composer-from-search-params.tsx | 10 ++- .../components/home/home-composer-intents.ts | 9 +++ .../home/home-composer-prompt-state.ts | 9 +++ .../home/use-home-composer-selection.ts | 16 +++- .../home/use-home-composer-submission.ts | 30 ++++---- .../components/projects/projects-shell.tsx | 49 +++++++++--- apps/web/src/lib/api/home-launch.ts | 23 ++++-- packages/agent-core/README.md | 6 +- packages/agent-core/src/mastra/context.ts | 2 + .../agent-core/src/mastra/system-prompt.ts | 76 +++++++++++++++++-- .../src/mastra/tool-defs/request-context.ts | 5 ++ packages/types/README.md | 3 +- packages/types/src/api.ts | 9 ++- 25 files changed, 304 insertions(+), 104 deletions(-) diff --git a/apps/agent-worker/README.md b/apps/agent-worker/README.md index f7ecec8e..a8b087be 100644 --- a/apps/agent-worker/README.md +++ b/apps/agent-worker/README.md @@ -73,7 +73,10 @@ the existing account state and R2 lifecycle phases. Run creation validates the gateway payload with the shared `CreateRunSchema` from `packages/types` before selecting the run-scoped `AgentRun` Durable Object. The -database binds a gateway-hashed idempotency key to the exact body and thread. After the +validated request keeps the user's exact message separate from explicit non-app run intent, +selected skill, and selected connected-app metadata. Those selections remain in the +checkpointed Workflow input and request context; they are never encoded into visible prompt text. +The database binds a gateway-hashed idempotency key to the exact body and thread. After the pending run and thread pointer commit, start delivery is retried and then reconciled through an ordered run-key presence probe. A present object reconnects its stream (and finalizes a durable Workflow admission first); only an authoritative empty response fails the nonterminal database run diff --git a/apps/agent-worker/src/agent-routing.ts b/apps/agent-worker/src/agent-routing.ts index d0074564..63d83f3d 100644 --- a/apps/agent-worker/src/agent-routing.ts +++ b/apps/agent-worker/src/agent-routing.ts @@ -190,6 +190,8 @@ export async function startAgentRun( model: run.modelId, isModelExplicit, ...(body.intent ? { runIntent: body.intent } : {}), + ...(body.selectedSkill ? { selectedSkill: body.selectedSkill } : {}), + ...(body.selectedTool ? { selectedTool: body.selectedTool } : {}), ...(run.projectId ? { projectId: run.projectId } : {}), ...(run.workspaceSlug ? { workspaceSlug: run.workspaceSlug } : {}), ...(run.projectMode ? { projectMode: run.projectMode } : {}), diff --git a/apps/agent-worker/src/durable-objects/agent-run-mastra-context.ts b/apps/agent-worker/src/durable-objects/agent-run-mastra-context.ts index 4d0d4a00..09c74812 100644 --- a/apps/agent-worker/src/durable-objects/agent-run-mastra-context.ts +++ b/apps/agent-worker/src/durable-objects/agent-run-mastra-context.ts @@ -110,6 +110,8 @@ export function createAgentRequestContext( openrouterApiKey: credential.transportProvider === "openrouter" ? credential.apiKey : undefined, projectMode: input.projectMode, runIntent: input.runIntent, + selectedSkill: input.selectedSkill, + selectedTool: input.selectedTool, runId: input.runId, taskMessage: input.messageText, ...(isSkillCreator ? { userSkillCreator } : {}), diff --git a/apps/agent-worker/src/durable-objects/agent-run-schemas.ts b/apps/agent-worker/src/durable-objects/agent-run-schemas.ts index 08ff7ad7..14743e99 100644 --- a/apps/agent-worker/src/durable-objects/agent-run-schemas.ts +++ b/apps/agent-worker/src/durable-objects/agent-run-schemas.ts @@ -1,5 +1,9 @@ -import { CatalogModelIdSchema, LogicalModelIdSchema } from "@cheatcode/types"; -import { ProjectModeSchema, RunIntentSchema } from "@cheatcode/types/api"; +import { + CatalogModelIdSchema, + IntegrationNameSchema, + LogicalModelIdSchema, +} from "@cheatcode/types"; +import { ProjectModeSchema, RunIntentSchema, SelectedSkillSchema } from "@cheatcode/types/api"; import { z } from "zod"; export const StartRunInputSchema = z @@ -16,6 +20,8 @@ export const StartRunInputSchema = z // automatic provider fallback so a pinned model is never silently replaced. isModelExplicit: z.boolean(), runIntent: RunIntentSchema.optional(), + selectedSkill: SelectedSkillSchema.optional(), + selectedTool: IntegrationNameSchema.optional(), projectMode: ProjectModeSchema.default("general"), isFirstRun: z.boolean().default(false), agentDisplayName: z.string().trim().min(1).max(80).optional(), diff --git a/apps/web/README.md b/apps/web/README.md index 7fef8aae..a6994dcb 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -85,8 +85,10 @@ The composer keeps three independent product concepts separate: - `ComposerWorkIntentId` describes what the user wants to accomplish. Web app, mobile app, slides, research, data, documents, and media are discoverable - composer choices. These choices guide prompt context; they are not all project - modes. The generic Slides choice activates the general PPTX workflow, never the + composer choices. Non-app choices cross the run API as validated intent metadata, + so an explicit Documents choice cannot be reclassified as Web merely because the + requested document discusses a website. These choices are not all project modes. + The generic Slides choice activates the general PPTX workflow, never the fundraising-specific pitch-deck skill. Explicit PPTX and pitch-deck skill deep links both select the Slides intent while preserving the skill the user chose. - `AppBuildTarget` is only the runtime topology for generated applications: @@ -100,9 +102,12 @@ The composer keeps three independent product concepts separate: focus from Files. Explicit browser takeover still selects Browser. The selected tab is not persisted across unrelated chats. +Composer selections are request metadata: `@`-selected skills and connected apps are +validated separately from the exact user-authored text and cross the opaque chat handoff. +The client never rewrites a visible message with a slash command or internal toolkit preamble. Signed-out launch handoff validates and restores `buildTarget`, model, and -public GitHub repository state before chat creation. The opaque prompt and -constrained run intent use the same one-shot handoff path. No `surface` query +public GitHub repository state before chat creation. The opaque prompt, +constrained run intent, skill, and connected-app selection use the same one-shot handoff path. No `surface` query parameter or persisted `app` tab alias is supported. ## Env diff --git a/apps/web/src/components/chat/chat-panel-controller.ts b/apps/web/src/components/chat/chat-panel-controller.ts index b9e93220..c420695a 100644 --- a/apps/web/src/components/chat/chat-panel-controller.ts +++ b/apps/web/src/components/chat/chat-panel-controller.ts @@ -9,6 +9,7 @@ import type { ChatOnDataCallback, ChatStatus } from "ai"; import { useRouter } from "next/navigation"; import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; +import type { ChatSubmissionSelection } from "@/components/chat/chat-panel-submission"; import { mergeLoadedMessageHistory, type PendingSubmission, @@ -44,6 +45,8 @@ export interface ChatPanelProps { hasOlderMessages: boolean; initialMessages?: CheatcodeUIMessage[] | undefined; initialRunIntent?: import("@cheatcode/types/api").RunIntent | null | undefined; + initialSelectedSkill?: null | string | undefined; + initialSelectedTool?: import("@cheatcode/types").IntegrationName | null | undefined; isLoadingOlderMessages: boolean; latestModelId: null | string; onLoadOlderMessages: () => Promise; @@ -161,7 +164,6 @@ function usePanelSubmission( getToken: runtime.getToken, hasReceivedStreamDataRef: runtime.hasReceivedStreamDataRef, hasSubmittedRef: runtime.hasSubmittedRef, - initialRunIntent: input.initialRunIntent ?? null, onSubmitDraft: input.onSubmitDraft, pendingSubmissionRef: runtime.pendingSubmissionRef, project: input.project, @@ -178,7 +180,6 @@ function usePanelSubmission( [ input.activeRunId, input.onSubmitDraft, - input.initialRunIntent, input.project, input.threadId, input.threadTitle, @@ -213,11 +214,19 @@ function useAutoSubmitPrompt( if (runtime.store.draft.trim() !== prompt) { runtime.store.setDraft(input.threadId, prompt); } - if (submitText(prompt, input.project)) { + const selection: ChatSubmissionSelection = { + intent: input.initialRunIntent ?? null, + selectedSkill: input.initialSelectedSkill ?? null, + selectedTool: input.initialSelectedTool ?? null, + }; + if (submitText(prompt, input.project, selection)) { autoSubmittedPromptRef.current = prompt; } }, [ input.autoSubmitPrompt, + input.initialRunIntent, + input.initialSelectedSkill, + input.initialSelectedTool, input.project, input.threadId, runtime.store.draft, diff --git a/apps/web/src/components/chat/chat-panel-submission.ts b/apps/web/src/components/chat/chat-panel-submission.ts index 3068e882..96665b24 100644 --- a/apps/web/src/components/chat/chat-panel-submission.ts +++ b/apps/web/src/components/chat/chat-panel-submission.ts @@ -1,5 +1,5 @@ -import type { CheatcodeUIMessage } from "@cheatcode/types"; -import type { ProjectSummary, ThreadMessage } from "@cheatcode/types/api"; +import type { CheatcodeUIMessage, IntegrationName } from "@cheatcode/types"; +import type { ProjectSummary, RunIntent, ThreadMessage } from "@cheatcode/types/api"; import type { QueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { buildExistingProjectParams, launchIntoProject } from "@/lib/api/home-launch"; @@ -12,6 +12,12 @@ import { } from "@/lib/api/project-thread"; import { sidebarKeys, threadKeys } from "@/lib/api/query-keys"; +export interface ChatSubmissionSelection { + intent: RunIntent | null; + selectedSkill: string | null; + selectedTool: IntegrationName | null; +} + export interface PendingSubmission { messageId: string; restoreToComposer: boolean; @@ -70,6 +76,7 @@ export async function routePromptToProjectTarget(input: { queryClient: QueryClient; router: PromptRouter; selectedModel: null | string; + selection: Partial; setDraft: (threadId: string, value: string) => void; targetProject: ProjectSummary | null; threadId: string; @@ -144,7 +151,12 @@ function completeProjectTargetNavigation( input: Parameters[0], targetThreadId: string, ): void { - const handoff = buildExistingProjectParams(input.prompt).toString(); + const handoff = buildExistingProjectParams({ + prompt: input.prompt, + ...(input.selection.intent ? { intent: input.selection.intent } : {}), + ...(input.selection.selectedSkill ? { selectedSkill: input.selection.selectedSkill } : {}), + ...(input.selection.selectedTool ? { selectedTool: input.selection.selectedTool } : {}), + }).toString(); input.setDraft(input.threadId, ""); void input.queryClient.invalidateQueries({ queryKey: sidebarKeys.chats }); void input.queryClient.invalidateQueries({ queryKey: sidebarKeys.projectThreads }); diff --git a/apps/web/src/components/chat/chat-transport.ts b/apps/web/src/components/chat/chat-transport.ts index 581a2d89..5bbfcd52 100644 --- a/apps/web/src/components/chat/chat-transport.ts +++ b/apps/web/src/components/chat/chat-transport.ts @@ -47,6 +47,8 @@ export function createChatTransport( intent: body?.["intent"], message: runRequestMessage(latestMessage), model: body?.["model"], + selectedSkill: body?.["selectedSkill"], + selectedTool: body?.["selectedTool"], }, headers: { ...headers, diff --git a/apps/web/src/components/chat/prompt-composer-controller.ts b/apps/web/src/components/chat/prompt-composer-controller.ts index f4df0418..ba927a32 100644 --- a/apps/web/src/components/chat/prompt-composer-controller.ts +++ b/apps/web/src/components/chat/prompt-composer-controller.ts @@ -13,7 +13,6 @@ import { useState, } from "react"; import type { RunStatus } from "@/components/chat/status-pill"; -import { composePromptWithComposerContext } from "@/components/composer/composer-context-chips"; import type { ComposerMenuItem } from "@/components/composer/composer-popover"; import { type ComposerMenuController, @@ -30,7 +29,11 @@ type ComposerControlMenu = "model"; export interface PromptComposerProps { onChange: (value: string) => void; onStop: () => void; - onSubmit: (value: string, project: ProjectSummary | null) => boolean; + onSubmit: ( + value: string, + project: ProjectSummary | null, + selection: { selectedSkill: string | null; selectedTool: IntegrationName | null }, + ) => boolean; project: ProjectSummary | null; resolvedModelId: null | string; status: RunStatus; @@ -231,14 +234,10 @@ function createComposerSubmission({ value, }: ComposerSubmissionOptions) { function submitComposerValue() { - const wasAccepted = onSubmit( - composePromptWithComposerContext({ - prompt: value, - skill: selection.selectedSkill, - tool: selection.selectedTool, - }), - project, - ); + const wasAccepted = onSubmit(value.trim(), project, { + selectedSkill: selection.selectedSkill, + selectedTool: selection.selectedTool, + }); if (wasAccepted) { selection.setSelectedSkill(null); selection.setSelectedTool(null); diff --git a/apps/web/src/components/chat/use-chat-submission.ts b/apps/web/src/components/chat/use-chat-submission.ts index 6ab7326e..6a717adf 100644 --- a/apps/web/src/components/chat/use-chat-submission.ts +++ b/apps/web/src/components/chat/use-chat-submission.ts @@ -1,10 +1,11 @@ -import type { CheatcodeUIMessage } from "@cheatcode/types"; +import type { CheatcodeUIMessage, IntegrationName } from "@cheatcode/types"; import type { ProjectSummary, RunIntent } from "@cheatcode/types/api"; import type { QueryClient } from "@tanstack/react-query"; import type { ChatStatus } from "ai"; import { useCallback } from "react"; import { toast } from "sonner"; import { + type ChatSubmissionSelection, type PendingSubmission, type PromptRouter, routePromptToProjectTarget, @@ -23,7 +24,6 @@ interface ChatSubmissionInput { getToken: () => Promise; hasReceivedStreamDataRef: { current: boolean }; hasSubmittedRef: { current: boolean }; - initialRunIntent: RunIntent | null; onSubmitDraft?: (() => void) | undefined; pendingSubmissionRef: { current: PendingSubmission | null }; project: ProjectSummary | null; @@ -40,10 +40,18 @@ interface ChatSubmissionInput { export function useChatSubmission(input: ChatSubmissionInput): { continueRun: () => void; - submitText: (text: string, targetProject: ProjectSummary | null) => boolean; + submitText: ( + text: string, + targetProject: ProjectSummary | null, + selection?: Partial, + ) => boolean; } { const submitText = useCallback( - (text: string, targetProject: ProjectSummary | null): boolean => { + ( + text: string, + targetProject: ProjectSummary | null, + selection: Partial = {}, + ): boolean => { if (!isValidUserMessage(text)) { return false; } @@ -52,10 +60,10 @@ export function useChatSubmission(input: ChatSubmissionInput): { } input.hasSubmittedRef.current = true; if (!isCurrentThreadTarget(input.project, targetProject)) { - routeSubmissionToProject(input, text, targetProject); + routeSubmissionToProject(input, text, targetProject, selection); return true; } - submitInCurrentThread(input, text); + submitInCurrentThread(input, text, selection); return true; }, [input], @@ -73,16 +81,17 @@ export function useChatSubmission(input: ChatSubmissionInput): { const pending = pendingSubmission(messageId, text, false); input.pendingSubmissionRef.current = pending; input.setRunStartedAt(pending.submittedAt); - void input.sendMessage( - userMessage(messageId, text, null), - modelBody(input.selectedModel, null), - ); + void input.sendMessage(userMessage(messageId, text, null), modelBody(input.selectedModel, {})); }, [input]); return { continueRun, submitText }; } -function submitInCurrentThread(input: ChatSubmissionInput, text: string): void { +function submitInCurrentThread( + input: ChatSubmissionInput, + text: string, + selection: Partial, +): void { if (!input.threadTitle?.trim() || input.threadTitle.trim() === "New chat") { void titleChatFromFirstPrompt(input.getToken, input.queryClient, input.threadId, text); } @@ -92,8 +101,8 @@ function submitInCurrentThread(input: ChatSubmissionInput, text: string): void { input.pendingSubmissionRef.current = pending; input.setRunStartedAt(pending.submittedAt); void input.sendMessage( - userMessage(messageId, text, input.initialRunIntent), - modelBody(input.selectedModel, input.initialRunIntent), + userMessage(messageId, text, selection.intent ?? null), + modelBody(input.selectedModel, selection), ); input.setDraft(input.threadId, ""); input.onSubmitDraft?.(); @@ -103,6 +112,7 @@ function routeSubmissionToProject( input: ChatSubmissionInput, prompt: string, targetProject: ProjectSummary | null, + selection: Partial, ): void { void routePromptToProjectTarget({ getToken: input.getToken, @@ -110,6 +120,7 @@ function routeSubmissionToProject( queryClient: input.queryClient, router: input.router, selectedModel: input.selectedModel, + selection, setDraft: input.setDraft, targetProject, threadId: input.threadId, @@ -176,12 +187,21 @@ function userMessage( function modelBody( selectedModel: null | string, - intent: RunIntent | null, -): { body: { intent?: RunIntent; model?: string } } { + selection: Partial, +): { + body: { + intent?: RunIntent; + model?: string; + selectedSkill?: string; + selectedTool?: IntegrationName; + }; +} { return { body: { - ...(intent ? { intent } : {}), + ...(selection.intent ? { intent: selection.intent } : {}), ...(selectedModel ? { model: selectedModel } : {}), + ...(selection.selectedSkill ? { selectedSkill: selection.selectedSkill } : {}), + ...(selection.selectedTool ? { selectedTool: selection.selectedTool } : {}), }, }; } diff --git a/apps/web/src/components/composer/composer-context-chips.tsx b/apps/web/src/components/composer/composer-context-chips.tsx index e3011647..cbac8097 100644 --- a/apps/web/src/components/composer/composer-context-chips.tsx +++ b/apps/web/src/components/composer/composer-context-chips.tsx @@ -25,26 +25,6 @@ function toolLabel(slug: string): string { ); } -export function composePromptWithComposerContext({ - prompt, - skill, - tool, -}: { - prompt: string; - skill: string | null; - tool: IntegrationName | null; -}): string { - const trimmed = prompt.trim(); - let nextPrompt = skill && !trimmed.startsWith("/") ? `/${skill} ${trimmed}` : trimmed; - if (tool) { - nextPrompt = [ - `Selected tool: ${toolLabel(tool)} (${tool}). Use the Composio integration for this request when an external app action is needed.`, - nextPrompt, - ].join("\n\n"); - } - return nextPrompt.trim(); -} - export function ComposerContextChips({ className, onClearSkill, diff --git a/apps/web/src/components/home/home-composer-controller.ts b/apps/web/src/components/home/home-composer-controller.ts index 6d202484..57fc46f7 100644 --- a/apps/web/src/components/home/home-composer-controller.ts +++ b/apps/web/src/components/home/home-composer-controller.ts @@ -1,6 +1,7 @@ "use client"; import type { IntegrationName } from "@cheatcode/types"; +import type { RunIntent } from "@cheatcode/types/api"; import { useAuth } from "@clerk/nextjs"; import { useRouter } from "next/navigation"; import { @@ -29,6 +30,7 @@ export interface HomeComposerProps { initialModel?: AgentModelId | undefined; initialPromptKey?: string | undefined; initialRepoUrl?: string | undefined; + initialRunIntent?: RunIntent | undefined; initialSkill?: string | undefined; initialTool?: IntegrationName | undefined; quickActionsSlot?: HTMLElement | null | undefined; @@ -93,6 +95,7 @@ function useInitialHomeSelection(input: HomeComposerProps, focusTextarea: () => appBuildTarget: input.initialAppBuildTarget ?? null, initialSkill, initialTool: input.initialTool ?? null, + runIntent: input.initialRunIntent ?? null, repoUrl: input.initialRepoUrl ?? null, skillCreator: input.skillCreator ?? false, }, diff --git a/apps/web/src/components/home/home-composer-from-search-params.tsx b/apps/web/src/components/home/home-composer-from-search-params.tsx index bc59b397..a1da36e6 100644 --- a/apps/web/src/components/home/home-composer-from-search-params.tsx +++ b/apps/web/src/components/home/home-composer-from-search-params.tsx @@ -2,7 +2,7 @@ import { SKILL_MANIFEST } from "@cheatcode/skills/manifest"; import { type IntegrationName, IntegrationNameSchema } from "@cheatcode/types"; -import { GitHubRepoUrlSchema } from "@cheatcode/types/api"; +import { GitHubRepoUrlSchema, type RunIntent, RunIntentSchema } from "@cheatcode/types/api"; import { useEffect, useState } from "react"; import { type AgentModelId, isAgentModelId } from "@/lib/agent-models"; import { type AppBuildTarget, isAppBuildTarget } from "@/lib/app-build-target"; @@ -14,6 +14,7 @@ type InitialComposerParams = { model?: AgentModelId | undefined; promptKey?: string | undefined; repoUrl?: string | undefined; + runIntent?: RunIntent | undefined; skill?: string | undefined; skillCreator?: boolean | undefined; tool?: IntegrationName | undefined; @@ -34,6 +35,7 @@ export function HomeComposerFromSearchParams({ model: validInitialModel(searchParams.get("model")), promptKey: validInitialPromptKey(searchParams.get("promptKey")), repoUrl: validInitialRepoUrl(searchParams.get("repo")), + runIntent: validRunIntent(searchParams.get("intent")), skill: validInitialSkill(searchParams.get("skill")), skillCreator: searchParams.get("intent") === "skill-creator", tool: validInitialTool(searchParams.get("tool")), @@ -50,6 +52,7 @@ export function HomeComposerFromSearchParams({ initialModel={params.model} initialPromptKey={params.promptKey} initialRepoUrl={params.repoUrl} + initialRunIntent={params.runIntent} initialSkill={params.skill} initialTool={params.tool} key={`${resetToken}:${params.promptKey ?? ""}:${params.skill ?? ""}:${params.tool ?? ""}:${params.appBuildTarget ?? ""}:${params.model ?? ""}:${params.repoUrl ?? ""}:${params.skillCreator ? "sc" : ""}`} @@ -84,6 +87,11 @@ function validInitialTool(value: string | null): IntegrationName | undefined { return result.success ? result.data : undefined; } +function validRunIntent(value: string | null): RunIntent | undefined { + const result = RunIntentSchema.safeParse(value); + return result.success ? result.data : undefined; +} + function validInitialPromptKey(value: string | null): string | undefined { if (value && /^[\w-]{8,80}$/.test(value)) { return value; diff --git a/apps/web/src/components/home/home-composer-intents.ts b/apps/web/src/components/home/home-composer-intents.ts index 9ea3dcb2..bf807401 100644 --- a/apps/web/src/components/home/home-composer-intents.ts +++ b/apps/web/src/components/home/home-composer-intents.ts @@ -1,3 +1,4 @@ +import type { RunIntent } from "@cheatcode/types/api"; import type { ComponentType } from "react"; import type { ComposerWorkIntentId } from "@/components/home/home-composer.types"; import { skillAppBuildTarget } from "@/components/home/use-initial-skill"; @@ -10,6 +11,7 @@ export type ComposerWorkIntent = { id: ComposerWorkIntentId; label: string; placeholder: string; + runIntent: RunIntent | null; skill: null | string; appBuildTarget: AppBuildTarget | null; }; @@ -21,6 +23,7 @@ export const COMPOSER_WORK_INTENTS: readonly ComposerWorkIntent[] = [ id: "mobile-app", label: "Mobile app", placeholder: "Describe the app - I'll build it with a live phone preview", + runIntent: null, skill: null, }, { @@ -29,6 +32,7 @@ export const COMPOSER_WORK_INTENTS: readonly ComposerWorkIntent[] = [ id: "web-app", label: "Web app", placeholder: "Describe the site or web app - I'll build and preview it", + runIntent: null, skill: null, }, { @@ -37,6 +41,7 @@ export const COMPOSER_WORK_INTENTS: readonly ComposerWorkIntent[] = [ id: "slides", label: "Slides", placeholder: "What's the deck about? Audience and key points help", + runIntent: "slides", skill: "pptx", }, { @@ -45,6 +50,7 @@ export const COMPOSER_WORK_INTENTS: readonly ComposerWorkIntent[] = [ id: "research", label: "Research", placeholder: "What should I research? I'll fan out agents and cite sources", + runIntent: "research", skill: "deep-research", }, { @@ -53,6 +59,7 @@ export const COMPOSER_WORK_INTENTS: readonly ComposerWorkIntent[] = [ id: "data", label: "Data", placeholder: "Attach or describe the data - I'll profile and chart it", + runIntent: "data", skill: "csv-analyst", }, { @@ -61,6 +68,7 @@ export const COMPOSER_WORK_INTENTS: readonly ComposerWorkIntent[] = [ id: "documents", label: "Documents", placeholder: "Describe the report, memo, PDF, or document you need", + runIntent: "documents", skill: null, }, { @@ -69,6 +77,7 @@ export const COMPOSER_WORK_INTENTS: readonly ComposerWorkIntent[] = [ id: "media", label: "Media", placeholder: "Describe the image or video you want to create or edit", + runIntent: "media", skill: "generate-media", }, ] as const; diff --git a/apps/web/src/components/home/home-composer-prompt-state.ts b/apps/web/src/components/home/home-composer-prompt-state.ts index f9216b15..77cc05d5 100644 --- a/apps/web/src/components/home/home-composer-prompt-state.ts +++ b/apps/web/src/components/home/home-composer-prompt-state.ts @@ -1,5 +1,6 @@ "use client"; +import type { IntegrationName } from "@cheatcode/types"; import type { RunIntent } from "@cheatcode/types/api"; import type { AppBuildTarget } from "@/lib/app-build-target"; import { createPromptHandoff } from "@/lib/input/prompt-handoff"; @@ -10,6 +11,8 @@ export function buildLaunchParams(input: { model: null | string; prompt: string; repo: null | string; + selectedSkill: null | string; + selectedTool: IntegrationName | null; }): URLSearchParams { const params = new URLSearchParams(); if (input.intent) { @@ -27,6 +30,12 @@ export function buildLaunchParams(input: { if (input.repo) { params.set("repo", input.repo); } + if (input.selectedSkill) { + params.set("skill", input.selectedSkill); + } + if (input.selectedTool) { + params.set("tool", input.selectedTool); + } return params; } diff --git a/apps/web/src/components/home/use-home-composer-selection.ts b/apps/web/src/components/home/use-home-composer-selection.ts index 805dd191..c66cab35 100644 --- a/apps/web/src/components/home/use-home-composer-selection.ts +++ b/apps/web/src/components/home/use-home-composer-selection.ts @@ -1,7 +1,7 @@ "use client"; import type { IntegrationName } from "@cheatcode/types"; -import type { ProjectSummary } from "@cheatcode/types/api"; +import type { ProjectSummary, RunIntent } from "@cheatcode/types/api"; import { useCallback, useState } from "react"; import type { ComposerWorkIntentId } from "@/components/home/home-composer.types"; import { COMPOSER_WORK_INTENTS } from "@/components/home/home-composer-intents"; @@ -13,6 +13,7 @@ interface InitialSelection { initialSkill: ReturnType; initialTool: IntegrationName | null; repoUrl: string | null; + runIntent: RunIntent | null; skillCreator: boolean; } @@ -36,7 +37,9 @@ export function useHomeComposerSelection(initial: InitialSelection, focusTextare function useHomeSelectionState(initial: InitialSelection) { const [intentId, setIntentId] = useState( - initial.initialSkill.intent ?? appBuildTargetIntent(initial.appBuildTarget), + initial.initialSkill.intent ?? + appBuildTargetIntent(initial.appBuildTarget) ?? + runIntentWorkIntent(initial.runIntent), ); const [skillChip, setSkillChip] = useState(initial.initialSkill.chip); const [toolChip, setToolChip] = useState(initial.initialTool); @@ -102,6 +105,15 @@ function appBuildTargetIntent(target: AppBuildTarget | null): ComposerWorkIntent return target === "web" ? "web-app" : null; } +function runIntentWorkIntent(intent: RunIntent | null): ComposerWorkIntentId | null { + if (intent === "data") return "data"; + if (intent === "documents") return "documents"; + if (intent === "media") return "media"; + if (intent === "research") return "research"; + if (intent === "slides") return "slides"; + return null; +} + function useResourceSelectionActions(state: ReturnType) { const { intentId, setIntentId, setRepoUrl, setSelectedProject, setSkillChip } = state; const handleRepoAttach = useCallback( diff --git a/apps/web/src/components/home/use-home-composer-submission.ts b/apps/web/src/components/home/use-home-composer-submission.ts index 32a47603..d613f3e6 100644 --- a/apps/web/src/components/home/use-home-composer-submission.ts +++ b/apps/web/src/components/home/use-home-composer-submission.ts @@ -4,7 +4,6 @@ import type { IntegrationName } from "@cheatcode/types"; import type { ProjectSummary, RunIntent } from "@cheatcode/types/api"; import { useCallback, useRef, useState } from "react"; import { toast } from "sonner"; -import { composePromptWithComposerContext } from "@/components/composer/composer-context-chips"; import type { ComposerWorkIntentId } from "@/components/home/home-composer.types"; import type { ComposerWorkIntent } from "@/components/home/home-composer-intents"; import { @@ -37,6 +36,8 @@ interface HomeSubmissionSnapshot { project: ProjectSummary | null; prompt: string; repoUrl: string | null; + selectedSkill: string | null; + selectedTool: IntegrationName | null; } interface SubmissionRuntime { @@ -83,12 +84,7 @@ export function useHomeComposerSubmission(input: { } function buildSubmissionSnapshot(state: HomeSubmissionState): HomeSubmissionSnapshot | null { - const skill = resolveSubmitSkill(state.repoUrl, state.intent, state.skillChip); - const prompt = composePromptWithComposerContext({ - prompt: state.value.trim(), - skill, - tool: state.toolChip, - }); + const prompt = state.value.trim(); try { assertUserMessageWithinLimit(prompt); } catch (error) { @@ -102,11 +98,13 @@ function buildSubmissionSnapshot(state: HomeSubmissionState): HomeSubmissionSnap state.intent, state.skillChip, ), - intent: state.skillCreatorMode ? "skill-creator" : null, + intent: state.skillCreatorMode ? "skill-creator" : (state.intent?.runIntent ?? null), model: agentModelRequestValue(state.agentModelId) ?? null, project: state.selectedProject, prompt, repoUrl: state.repoUrl, + selectedSkill: resolveSubmitSkill(state.repoUrl, state.intent, state.skillChip), + selectedTool: state.repoUrl ? null : state.toolChip, }; } @@ -135,7 +133,7 @@ async function launchExistingProject( runtime.endSubmitting(); return; } - navigateToChat(runtime, result.threadId, snapshot.prompt, snapshot.intent); + navigateToChat(runtime, result.threadId, snapshot); } catch (error) { toast.error(error instanceof Error ? error.message : "Could not open that project."); runtime.endSubmitting(); @@ -159,7 +157,7 @@ async function startNewChat( ...(snapshot.repoUrl ? { importRepoUrl: snapshot.repoUrl } : {}), ...(snapshot.model ? { defaultModel: snapshot.model } : {}), }); - navigateToChat(runtime, thread.id, snapshot.prompt, snapshot.intent); + navigateToChat(runtime, thread.id, snapshot); } catch (error) { toast.error(error instanceof Error ? error.message : "Could not start that chat."); runtime.endSubmitting(); @@ -176,6 +174,8 @@ function preservePromptForSignIn( model: snapshot.model, prompt: snapshot.prompt, repo: snapshot.repoUrl, + selectedSkill: snapshot.selectedSkill, + selectedTool: snapshot.selectedTool, appBuildTarget: snapshot.appBuildTarget, }); runtime.setAuthRedirectTo(`/?${params.toString()}`); @@ -188,10 +188,14 @@ function preservePromptForSignIn( function navigateToChat( runtime: SubmissionRuntime, threadId: string, - prompt: string, - intent: RunIntent | null, + snapshot: HomeSubmissionSnapshot, ): void { - const handoff = buildExistingProjectParams(prompt, intent).toString(); + const handoff = buildExistingProjectParams({ + intent: snapshot.intent, + prompt: snapshot.prompt, + selectedSkill: snapshot.selectedSkill, + selectedTool: snapshot.selectedTool, + }).toString(); // A soft navigation may preserve this page in the Next.js route cache. Release // the submission lock before leaving so restoring the page can never revive a // permanently disabled composer. diff --git a/apps/web/src/components/projects/projects-shell.tsx b/apps/web/src/components/projects/projects-shell.tsx index 092a29ad..352a7b75 100644 --- a/apps/web/src/components/projects/projects-shell.tsx +++ b/apps/web/src/components/projects/projects-shell.tsx @@ -4,8 +4,15 @@ import { type CheatcodeUIMessage, coalesceTranscriptUIMessages, hasIncompleteTranscriptUIMessages, + IntegrationNameSchema, } from "@cheatcode/types"; -import type { ProjectSummary, RunIntent, Thread } from "@cheatcode/types/api"; +import { + type ProjectSummary, + type RunIntent, + RunIntentSchema, + SelectedSkillSchema, + type Thread, +} from "@cheatcode/types/api"; import { useAuth } from "@clerk/nextjs"; import { useInfiniteQuery, useQuery, useQueryClient } from "@tanstack/react-query"; import { parseAsString, useQueryStates } from "nuqs"; @@ -30,6 +37,8 @@ import { useAppStore } from "@/lib/store/app-store"; const PROMPT_URL_STATE = { intent: parseAsString, promptKey: parseAsString, + skill: parseAsString, + tool: parseAsString, } as const; export function ProjectsShell({ threadId: threadIdProp }: { threadId?: string }) { @@ -60,14 +69,13 @@ function useProjectsShell(threadIdProp: string | undefined) { () => chronologicalMessages(initialMessagesQuery.data?.pages ?? []), [initialMessagesQuery.data?.pages], ); - const { clearPromptParams, prompt, runIntent } = useInitialChatPrompt( - threadQuery.data ?? null, - initialMessagesQuery.isPending ? null : initialMessages, - ); - const hasProject = projectId !== null; + const { clearPromptParams, prompt, runIntent, selectedSkill, selectedTool } = + useInitialChatPrompt( + threadQuery.data ?? null, + initialMessagesQuery.isPending ? null : initialMessages, + ); const previewPanelOpen = useAppStore((state) => state.previewPanelOpen); const sandboxStatus = useAppStore((state) => state.sandboxStatus); - const hasComputer = hasProject || sandboxStatus !== "cold"; const deliverableCount = countDeliverables(initialMessages); const loadOlderMessages = useOlderThreadMessagesLoader(initialMessagesQuery); @@ -86,7 +94,7 @@ function useProjectsShell(threadIdProp: string | undefined) { initialMessagesQuery.hasTranscriptIntegrityError || projectQuery.isError || threadQuery.isError, - hasComputer, + hasComputer: projectId !== null || sandboxStatus !== "cold", initialMessages, initialMessagesQuery, isRetrying: retry.isRetrying, @@ -95,6 +103,8 @@ function useProjectsShell(threadIdProp: string | undefined) { projectQuery, prompt, runIntent, + selectedSkill, + selectedTool, retry: retry.run, threadId, threadQuery, @@ -168,6 +178,8 @@ function ProjectsWorkspace({ initialMessages={shell.initialMessages} isLoadingOlderMessages={shell.initialMessagesQuery.isFetchingNextPage} initialRunIntent={shell.runIntent} + initialSelectedSkill={shell.selectedSkill} + initialSelectedTool={shell.selectedTool} key={threadId} latestModelId={shell.threadQuery.data?.latestModelId ?? null} onLoadOlderMessages={shell.loadOlderMessages} @@ -197,7 +209,13 @@ function countDeliverables(messages: readonly CheatcodeUIMessage[]): number { function useInitialChatPrompt( thread: Thread | null, initialMessages: CheatcodeUIMessage[] | null, -): { clearPromptParams: () => void; prompt: null | string; runIntent: RunIntent | null } { +): { + clearPromptParams: () => void; + prompt: null | string; + runIntent: RunIntent | null; + selectedSkill: string | null; + selectedTool: import("@cheatcode/types").IntegrationName | null; +} { const [urlState, setUrlState] = useQueryStates(PROMPT_URL_STATE, { history: "replace", shallow: true, @@ -211,7 +229,12 @@ function useInitialChatPrompt( initialMessages, thread, }); - const runIntent = urlState.intent === "skill-creator" ? urlState.intent : null; + const parsedRunIntent = RunIntentSchema.safeParse(urlState.intent); + const runIntent = parsedRunIntent.success ? parsedRunIntent.data : null; + const parsedSkill = SelectedSkillSchema.safeParse(urlState.skill); + const selectedSkill = parsedSkill.success ? parsedSkill.data : null; + const parsedTool = IntegrationNameSchema.safeParse(urlState.tool); + const selectedTool = parsedTool.success ? parsedTool.data : null; const clearPromptParams = useCallback(() => { if (!hasPromptUrlState(urlState)) { return; @@ -219,9 +242,11 @@ function useInitialChatPrompt( void setUrlState({ intent: null, promptKey: null, + skill: null, + tool: null, }); }, [setUrlState, urlState]); - return { clearPromptParams, prompt, runIntent }; + return { clearPromptParams, prompt, runIntent, selectedSkill, selectedTool }; } function useRefreshThreadProjectOnSandboxChange(input: { @@ -245,7 +270,7 @@ function useRefreshThreadProjectOnSandboxChange(input: { function hasPromptUrlState( urlState: Record, ): boolean { - return Boolean(urlState.intent ?? urlState.promptKey); + return Boolean(urlState.intent ?? urlState.promptKey ?? urlState.skill ?? urlState.tool); } function useThreadQuery(getToken: () => Promise, threadId: null | string) { diff --git a/apps/web/src/lib/api/home-launch.ts b/apps/web/src/lib/api/home-launch.ts index bc70ab84..9185a6d9 100644 --- a/apps/web/src/lib/api/home-launch.ts +++ b/apps/web/src/lib/api/home-launch.ts @@ -1,3 +1,4 @@ +import type { IntegrationName } from "@cheatcode/types"; import type { RunIntent } from "@cheatcode/types/api"; import { createChat, listProjectThreadsPage, threadTitle } from "@/lib/api/project-thread"; import { createPromptHandoff } from "@/lib/input/prompt-handoff"; @@ -36,14 +37,22 @@ export async function launchIntoProject( * are already persisted on the newly created chat; this handoff carries only the * prompt and constrained run intent consumed by the chat workspace. */ -export function buildExistingProjectParams( - prompt: string, - intent: RunIntent | null = null, -): URLSearchParams { +export function buildExistingProjectParams(input: { + intent?: RunIntent | null; + prompt: string; + selectedSkill?: string | null; + selectedTool?: IntegrationName | null; +}): URLSearchParams { const params = new URLSearchParams(); - params.set("promptKey", createPromptHandoff(prompt).promptKey); - if (intent) { - params.set("intent", intent); + params.set("promptKey", createPromptHandoff(input.prompt).promptKey); + if (input.intent) { + params.set("intent", input.intent); + } + if (input.selectedSkill) { + params.set("skill", input.selectedSkill); + } + if (input.selectedTool) { + params.set("tool", input.selectedTool); } return params; } diff --git a/packages/agent-core/README.md b/packages/agent-core/README.md index de2eb4ee..0aeb548e 100644 --- a/packages/agent-core/README.md +++ b/packages/agent-core/README.md @@ -43,7 +43,11 @@ Tools execute autonomously inside the active request context. Sandbox operations remain project-root confined, browser actions remain origin-bound, connected-app actions remain scoped to the user's active account, and secret-bearing input is validated before execution. Deterministic prepare/execute boundaries keep dynamic -ports and Git destinations stable between resolution and execution. The managed browser follows +ports and Git destinations stable between resolution and execution. +Explicit composer intent is authoritative before message keyword classification on general-project +runs. A selected skill or connected app arrives as validated request context, prompts the agent to +load or use that exact capability, and never mutates the user's message into internal command syntax. +The managed browser follows the same boundary: observation reads Stagehand's native accessibility snapshot without model inference and returns page-bound element refs. Execution accepts only a single-use ref from the latest state tree plus a bounded method/value, resolves its server-held XPath, and atomically binds diff --git a/packages/agent-core/src/mastra/context.ts b/packages/agent-core/src/mastra/context.ts index 5f6daf6c..26578f29 100644 --- a/packages/agent-core/src/mastra/context.ts +++ b/packages/agent-core/src/mastra/context.ts @@ -25,6 +25,8 @@ export const CONTEXT = { researchWorkflowAttempted: "researchWorkflowAttempted", researchWorkflowActive: "researchWorkflowActive", runIntent: "runIntent", + selectedSkill: "selectedSkill", + selectedTool: "selectedTool", userSkillCreator: "userSkillCreator", userSkillLoader: "userSkillLoader", userSkills: "userSkills", diff --git a/packages/agent-core/src/mastra/system-prompt.ts b/packages/agent-core/src/mastra/system-prompt.ts index 01cdb056..0615bf80 100644 --- a/packages/agent-core/src/mastra/system-prompt.ts +++ b/packages/agent-core/src/mastra/system-prompt.ts @@ -1,5 +1,11 @@ import { buildSystemPromptSection, getSkillByName, SKILLS } from "@cheatcode/skills"; -import { MAX_USER_SKILLS, type RunIntent } from "@cheatcode/types/api"; +import { + MAX_USER_SKILLS, + type RunIntent, + RunIntentSchema, + SelectedSkillSchema, +} from "@cheatcode/types/api"; +import { type IntegrationName, IntegrationNameSchema } from "@cheatcode/types/integrations"; import { CONTEXT } from "./context"; import type { UserSkillRuntime } from "./user-skill-runtime"; @@ -14,6 +20,8 @@ export interface PromptRuntimeContext { /** app-builder / app-builder-mobile / general — selects the domain module. */ projectMode?: string; runIntent?: RunIntent; + selectedSkill?: string; + selectedTool?: IntegrationName; /** The user's request text, classified to select domain modules on the general path. */ taskMessage?: string; /** The run's project folder in the sandbox (/workspace/) — the agent's working directory. */ @@ -67,9 +75,23 @@ export function promptRuntimeContextFromRequestContext( if (projectMode) { context.projectMode = projectMode; } - const runIntent = trimmedContextValue(requestContext, CONTEXT.runIntent); - if (runIntent === "skill-creator") { - context.runIntent = runIntent; + const runIntent = RunIntentSchema.safeParse( + trimmedContextValue(requestContext, CONTEXT.runIntent), + ); + if (runIntent.success) { + context.runIntent = runIntent.data; + } + const selectedSkill = SelectedSkillSchema.safeParse( + trimmedContextValue(requestContext, CONTEXT.selectedSkill), + ); + if (selectedSkill.success) { + context.selectedSkill = selectedSkill.data; + } + const selectedTool = IntegrationNameSchema.safeParse( + trimmedContextValue(requestContext, CONTEXT.selectedTool), + ); + if (selectedTool.success) { + context.selectedTool = selectedTool.data; } const taskMessage = trimmedContextValue(requestContext, CONTEXT.promptTaskMessage); if (taskMessage) { @@ -111,7 +133,13 @@ export function buildSystemPrompt(runtimeContext: PromptRuntimeContext = {}): st runtimeContext.workspaceDir ? `Your project workspace is \`${runtimeContext.workspaceDir}\`. Create, edit, and run everything there (it's your project's folder in the shared computer). Use it as the working directory for shell commands and the dev server.` : "", - ...selectDomainModules(runtimeContext.projectMode, runtimeContext.taskMessage), + ...selectDomainModules( + runtimeContext.projectMode, + runtimeContext.taskMessage, + runtimeContext.runIntent, + ), + buildSelectedSkillDirective(runtimeContext.selectedSkill), + buildSelectedToolDirective(runtimeContext.selectedTool), FINISHING, runtimeContext.globalMemory ? `## User Memory\n${runtimeContext.globalMemory}` : "", buildSystemPromptSection(GENERAL_SKILLS), @@ -152,6 +180,26 @@ function requiredBundledSkillInstructions(name: string): string { return skill.body; } +function buildSelectedSkillDirective(selectedSkill: string | undefined): string { + if (!selectedSkill) { + return ""; + } + return [ + "## User-selected skill", + `The user selected the ${JSON.stringify(selectedSkill)} skill in the composer. Before doing the work, call \`skill_invoke\` with exactly that skill name and follow the returned instructions. The user's visible message is their complete request; do not mention this internal selection or rewrite their request as a slash command.`, + ].join("\n"); +} + +function buildSelectedToolDirective(selectedTool: IntegrationName | undefined): string { + if (!selectedTool) { + return ""; + } + return [ + "## User-selected connected app", + `The user selected the connected ${JSON.stringify(selectedTool)} app in the composer. Use that integration for any external-app action required by the request. Discover the narrow action first, then execute it. Do not mention the internal toolkit slug unless the user asks.`, + ].join("\n"); +} + const CORE_IDENTITY = [ "You are Cheatcode — a generalist AI agent that gets real work done on its own computer.", "You build web apps, mobile apps, data analyses, documents, decks, and research, and you hand back finished, working deliverables — not instructions for the user to follow.", @@ -272,17 +320,33 @@ const DOMAIN_MODULES: Record = { * path classifies the message and falls back to the compact all-domains pointer when the * intent is ambiguous (so the model is never blind — the classifier favours precision). */ -function selectDomainModules(projectMode?: string, taskMessage?: string): string[] { +function selectDomainModules( + projectMode?: string, + taskMessage?: string, + runIntent?: RunIntent, +): string[] { if (projectMode === "app-builder") { return [WEB_MODULE, APP_BUILDER_PREVIEW_NOTE]; } if (projectMode === "app-builder-mobile") { return [MOBILE_MODULE, APP_BUILDER_PREVIEW_NOTE]; } + const intentDomain = domainForRunIntent(runIntent); + if (intentDomain) { + return [DOMAIN_MODULES[intentDomain]]; + } const domains = classifyDomains(taskMessage ?? ""); return domains.length > 0 ? domains.map((domain) => DOMAIN_MODULES[domain]) : [GENERALIST_MODULE]; } +function domainForRunIntent(runIntent: RunIntent | undefined): DomainKey | null { + if (runIntent === "data") return "data"; + if (runIntent === "documents" || runIntent === "slides") return "docs"; + if (runIntent === "media") return "media"; + if (runIntent === "research") return "research"; + return null; +} + const DOMAIN_PATTERNS: ReadonlyArray = [ [ "media", diff --git a/packages/agent-core/src/mastra/tool-defs/request-context.ts b/packages/agent-core/src/mastra/tool-defs/request-context.ts index 274b8532..ecc2a284 100644 --- a/packages/agent-core/src/mastra/tool-defs/request-context.ts +++ b/packages/agent-core/src/mastra/tool-defs/request-context.ts @@ -1,6 +1,7 @@ import type { MorphApplyRuntime } from "@cheatcode/morph"; import type { CodeRuntimeContext } from "@cheatcode/sandbox-contracts"; import type { RunIntent } from "@cheatcode/types/api"; +import type { IntegrationName } from "@cheatcode/types/integrations"; import { RequestContext } from "@mastra/core/request-context"; import type { ComposioConnectedAccounts, ComposioQuotaMeter } from "../composio-context"; import { CONTEXT, type ContextKey } from "../context"; @@ -30,6 +31,8 @@ interface CodeRequestContextOptions { openrouterApiKey?: string | undefined; projectMode?: string | undefined; runIntent?: RunIntent | undefined; + selectedSkill?: string | undefined; + selectedTool?: IntegrationName | undefined; runId?: string | undefined; taskMessage?: string | undefined; userSkills?: UserSkillRuntime[] | undefined; @@ -61,6 +64,8 @@ function contextEntries( [CONTEXT.globalMemory, options.globalMemory], [CONTEXT.promptProjectMode, options.projectMode], [CONTEXT.runIntent, options.runIntent], + [CONTEXT.selectedSkill, options.selectedSkill], + [CONTEXT.selectedTool, options.selectedTool], [CONTEXT.promptTaskMessage, options.taskMessage], [CONTEXT.anthropicApiKey, options.anthropicApiKey], [CONTEXT.composioApiKey, options.composioApiKey], diff --git a/packages/types/README.md b/packages/types/README.md index adb19241..5b637811 100644 --- a/packages/types/README.md +++ b/packages/types/README.md @@ -32,7 +32,8 @@ capability discovery contracts, error codes, and UI message types. The `./api` subpath also exports the canonical user-message character budget, project-file upload/batch/namespace limits and schemas, the discriminated upload/generated-Deliverable project file catalog plus deterministic Deliverable path builder, and finalized project-archive byte -limit so browser and Worker boundaries cannot drift. +limit so browser and Worker boundaries cannot drift. `CreateRunSchema` keeps the exact user message +separate from validated non-app run intent, selected-skill, and connected-app metadata. ## Code Checks diff --git a/packages/types/src/api.ts b/packages/types/src/api.ts index 59f170e4..52f54f64 100644 --- a/packages/types/src/api.ts +++ b/packages/types/src/api.ts @@ -34,10 +34,13 @@ export const GitHubRepoUrlSchema = z const PROJECT_MODES = ["app-builder", "app-builder-mobile", "general"] as const; export const ProjectModeSchema = z.enum(PROJECT_MODES); -/** Product modes selected by UI intent or a high-confidence projectless build imperative. */ -const RUN_INTENTS = ["skill-creator"] as const; +/** Explicit non-app work paths selected by the composer; app topology remains a project mode. */ +const RUN_INTENTS = ["data", "documents", "media", "research", "skill-creator", "slides"] as const; export const RunIntentSchema = z.enum(RUN_INTENTS); +/** Skill selected through the `@` picker or a curated work-intent shortcut. */ +export const SelectedSkillSchema = z.string().trim().min(1).max(80); + export const CreateProjectSchema = z.strictObject({ defaultModel: LogicalModelIdSchema.optional(), importRepoUrl: GitHubRepoUrlSchema.optional(), @@ -215,6 +218,8 @@ export const CreateRunSchema = z.strictObject({ parts: z.array(UserTextPartSchema).length(1), }), model: LogicalModelIdSchema.optional(), + selectedSkill: SelectedSkillSchema.optional(), + selectedTool: IntegrationNameSchema.optional(), }); export const ProviderSchema = z.enum([