diff --git a/ui/storybook/prototypes/jev-task-creation/JevTaskComposer.tsx b/ui/storybook/prototypes/jev-task-creation/JevTaskComposer.tsx index 29a5632f33e..d40c54ea05f 100644 --- a/ui/storybook/prototypes/jev-task-creation/JevTaskComposer.tsx +++ b/ui/storybook/prototypes/jev-task-creation/JevTaskComposer.tsx @@ -13,7 +13,6 @@ import { JEV_MIN_PROMPT_LENGTH, JEV_MODEL, JEV_NAME, - JEV_NO_PROJECT, JEV_PROJECTS, agentContextCoverage, stabilizeRouting, @@ -26,7 +25,7 @@ import { export type JevTaskComposerProps = { /** - * Both flows update mode, owner, and project live while you type. The title is + * Both flows update mode and owner live while you type. The title is * drafted once, when the task is started (Start task or Enter), like Claude * Code naming a session after the first message. * `suggest-first`: review the live suggestions, then create. @@ -173,7 +172,6 @@ export function JevTaskComposer({ const changed: Partial> = previous ? { workMode: previous.workMode !== next.workMode, assignee: previous.assigneeId !== next.assigneeId, - project: previous.projectId !== next.projectId, } : {}; routingRef.current = next; setRouting(next); @@ -241,7 +239,8 @@ export function JevTaskComposer({ const effective = useMemo(() => ({ title: overrides.title ?? suggestedTitle ?? "", assignee: overrides.assignee ?? routing?.assigneeId ?? null, - project: overrides.project !== undefined ? overrides.project : routing?.projectId ?? null, + // Project is not predicted; it stays a manual choice. + project: overrides.project ?? null, workMode: overrides.workMode ?? routing?.workMode ?? "standard", }), [overrides, routing, suggestedTitle]); @@ -517,7 +516,7 @@ function RoutingPanel({ return `${JEV_NAME} wasn't sure who should own this (${percent(routing!.confidence.assignee)}), so it goes to ${fallbackOwner.name}, ${why}.`; } if (status === "idle" && promptLength === 0 && !created) { - return `As you type, ${JEV_NAME} picks the mode, owner, and project. The title is written when you ${flow === "instant" ? "start" : "create"} the task.`; + return `As you type, ${JEV_NAME} picks the mode and owner. The title is written when you ${flow === "instant" ? "start" : "create"} the task.`; } if (status === "idle" && promptLength > 0 && promptLength < JEV_MIN_PROMPT_LENGTH) return "Keep going. Suggestions start once there's a bit more to go on."; return null; @@ -596,10 +595,8 @@ function RoutingPanel({ {JEV_PROJECTS.find((project) => project.id === effective.project)?.name ?? "No project"}} > {(close) => [ @@ -607,7 +604,6 @@ function RoutingPanel({ { setField("project", project.id); close(); }} > {project.name} @@ -616,7 +612,6 @@ function RoutingPanel({ { setField("project", null); close(); }} > No project @@ -652,7 +647,7 @@ function RoutingPanel({ ); } -const FIELD_LABELS: Record = { workMode: "Mode", assignee: "Assignee", project: "Project" }; +const FIELD_LABELS: Record = { workMode: "Mode", assignee: "Assignee" }; function choiceLabel(field: JevField, routing: JevRouting): string { if (field === "assignee") { @@ -661,12 +656,11 @@ function choiceLabel(field: JevField, routing: JevRouting): string { ? `${name(routing.assigneeId)} (fallback; ${JEV_NAME} leaned ${name(routing.jevAssigneeId)})` : name(routing.assigneeId); } - if (field === "project") return JEV_PROJECTS.find((project) => project.id === routing.projectId)?.name ?? "No project"; return workModeMetaFor(routing.workMode).label; } function ConfidenceDetails({ routing, prompt, agents, liveStats }: { routing: JevRouting; prompt: string; agents: typeof JEV_AGENTS; liveStats: LiveStats }) { - const fields: JevField[] = ["workMode", "assignee", "project"]; + const fields: JevField[] = ["workMode", "assignee"]; return (
diff --git a/ui/storybook/prototypes/jev-task-creation/README.md b/ui/storybook/prototypes/jev-task-creation/README.md index c7b2fa9cb90..9305bc04290 100644 --- a/ui/storybook/prototypes/jev-task-creation/README.md +++ b/ui/storybook/prototypes/jev-task-creation/README.md @@ -17,8 +17,8 @@ Two models do the work, because they are good at different things: - **[Jev](https://openrouter.ai/docs/guides/community/jev)** (`typesafe/jev-1.13`, TypeSafe's decision model on OpenRouter) answers typed `choice` questions for - the task's **mode**, which is its type (Auto, Plan, or Ask), plus its - **assignee** and **project**. It returns a choice, a + the task's **mode**, which is its type (Auto, Plan, or Ask), and its + **assignee**. It returns a choice, a confidence, and a probability for every option. It is fast and cheap: you pay for input tokens only, and output tokens are free. It does not generate text or explain its answers. @@ -27,7 +27,7 @@ Two models do the work, because they are good at different things: ## What the prototype shows -- **Live routing while you type.** Mode, assignee, and project re-route about +- **Live routing while you type.** Mode and assignee re-route about 250 ms after each pause in typing, and at least every 800 ms during continuous typing. Jev is fast and cheap enough for this: a call costs about $0.00004. - **Steady suggestions.** A chip only switches when Jev's new pick leads the @@ -41,6 +41,9 @@ Two models do the work, because they are good at different things: **Enter**, like Claude Code naming a session after the first message. It types into the header breadcrumb and you can click it to edit. Shift+Enter adds a new line. +- **Project is not predicted.** It stays a plain manual picker (default "No + project"). Project leads still appear in each agent's description, because + they help Jev pick the owner. - **Mode is the task's type** and uses the shipped `IssueWorkMode` values and labels: - **Auto** (`standard`): the agent does the work. @@ -74,8 +77,8 @@ Two models do the work, because they are good at different things: ## What's real and what's simulated `jev-classifier.ts` builds the real Decisions API request body -(`buildJevDecisionRequest`): the prompt, the company's agents and projects in -`state`, and three `choice` questions. The request and response types follow the +(`buildJevDecisionRequest`): the prompt and the company's agents in `state`, +and two `choice` questions. The request and response types follow the OpenRouter docs. Each agent's option in the assignee question is described by @@ -106,7 +109,7 @@ too (`draftTitle`). No API is called and no task is persisted. 1. Add a company-scoped server route, for example `POST /api/companies/:companyId/issues/suggest`. It builds the request from - the company's active agents and projects, leaving out paused agents and agents + the company's active agents and their context, leaving out paused agents and agents over budget. It calls `POST https://openrouter.ai/api/alpha/decisions` with the instance's OpenRouter key, and a small chat model for the title, in parallel. Keep the key on the server. diff --git a/ui/storybook/prototypes/jev-task-creation/jev-classifier.ts b/ui/storybook/prototypes/jev-task-creation/jev-classifier.ts index b22c042eb8a..4ac1f013f8b 100644 --- a/ui/storybook/prototypes/jev-task-creation/jev-classifier.ts +++ b/ui/storybook/prototypes/jev-task-creation/jev-classifier.ts @@ -8,8 +8,7 @@ import type { IssueWorkMode } from "@paperclipai/shared"; * does not generate text or explain itself. So routing is split in two: * * - Jev (`POST /api/alpha/decisions`, model `typesafe/jev-1.13`) answers `choice` - * questions for the task's mode (its type: Auto, Plan, or Ask), assignee, and - * project. + * questions for the task's mode (its type: Auto, Plan, or Ask) and assignee. * - A small text model drafts the title, the way Claude Code names a session. * * `buildJevDecisionRequest` produces the real request body. The response is @@ -191,7 +190,6 @@ export function agentContextCoverage(agents: JevAgent[], projects: JevProject[] }; } -export const JEV_NO_PROJECT = "none"; // --------------------------------------------------------------------------- // Request builder: this is what a server would send to OpenRouter. @@ -205,7 +203,6 @@ export function buildJevDecisionRequest(prompt: string, agents: JevAgent[] = JEV task_prompt: prompt, // Each agent's full description lives in the assignee criteria, so it is sent once. agents: available.map(({ id, name, title, reportsTo }) => ({ id, name, title, reports_to: reportsTo ?? "board" })), - projects: JEV_PROJECTS.map(({ id, name, description, leadAgentId }) => ({ id, name, description, lead_agent_id: leadAgentId })), }, questions: { work_mode: { @@ -222,14 +219,6 @@ export function buildJevDecisionRequest(prompt: string, agents: JevAgent[] = JEV instructions: "Which agent should own this task?", criteria: Object.fromEntries(available.map((agent) => [agent.id, describeAgentForJev(agent)])), }, - project: { - type: "choice", - instructions: "Which project does this task belong to?", - criteria: { - ...Object.fromEntries(JEV_PROJECTS.map((project) => [project.id, project.description])), - [JEV_NO_PROJECT]: "None of the projects fit.", - }, - }, }, }; } @@ -259,12 +248,6 @@ const AREA_OWNER: Record = { ops: "agent-darnold", }; -const PROJECT_KEYWORDS: Record = { - "project-board-ui": ["ui", "board", "dashboard", "page", "button", "dialog", "sidebar", "inbox", "screen", "mobile", "login", "storybook"], - "project-agent-runtime": ["agent", "runtime", "heartbeat", "adapter", "wake", "codex", "claude", "runner", "session"], - "project-budget-guardrails": ["budget", "spend", "cost", "billing", "limit", "invoice", "quota"], -}; - function score(text: string, keywords: string[]): number { return keywords.reduce((total, keyword) => total + (text.includes(keyword) ? 1 : 0), 0); } @@ -310,11 +293,6 @@ export function simulateJevDecisions(request: JevDecisionRequest): JevDecisionRe const assigneeTotal = Object.values(assigneeProbabilities).reduce((sum, value) => sum + value, 0); for (const id of Object.keys(assigneeProbabilities)) assigneeProbabilities[id]! /= assigneeTotal; - const projectScores: Record = { - ...Object.fromEntries(Object.entries(PROJECT_KEYWORDS).map(([id, keywords]) => [id, score(text, keywords)])), - [JEV_NO_PROJECT]: 0.5, - }; - const isQuestion = /\?\s*$/.test(prompt.trim()) || (/^(how|why|what|which|should|is|are|can|does)\b/.test(text) && !/^(can|could|would) you\b/.test(text)); const isLarge = /\b(plan|roadmap|migrate|migration|redesign|rewrite|overhaul|multi-step|phases?)\b/.test(text); const isInvestigation = /^(investigate|research|find out|figure out|look into|compare|evaluate)\b/.test(text); @@ -327,7 +305,6 @@ export function simulateJevDecisions(request: JevDecisionRequest): JevDecisionRe answers: { work_mode: choiceAnswer(softmax(modeScores)), assignee: choiceAnswer(assigneeProbabilities), - project: choiceAnswer(softmax(projectScores, 2.4)), }, // Output tokens are free on Jev; $0.042 per million input tokens. usage: { input_tokens: inputTokens, output_tokens: 0, cost: (inputTokens * 0.042) / 1_000_000 }, @@ -337,7 +314,7 @@ export function simulateJevDecisions(request: JevDecisionRequest): JevDecisionRe // --------------------------------------------------------------------------- // What the composer consumes -export type JevField = "workMode" | "assignee" | "project"; +export type JevField = "workMode" | "assignee"; /** * Who gets a task when Jev isn't confident about the owner: the org's first @@ -356,7 +333,6 @@ export type JevRouting = { assigneeSource: "jev" | "fallback"; /** Jev's own top pick, kept even when the fallback is used. */ jevAssigneeId: string; - projectId: string | null; confidence: Record; probabilities: Record>; /** Jev's likely owners other than the one assigned, offered when Jev is unsure. */ @@ -365,7 +341,7 @@ export type JevRouting = { }; export function routingFromJev(response: JevDecisionResponse, agents: JevAgent[] = JEV_AGENTS): JevRouting { - const { work_mode: mode, assignee, project } = response.answers as Record; + const { work_mode: mode, assignee } = response.answers as Record; const fallbackId = assignee!.confidence < JEV_CONFIDENCE_THRESHOLD ? fallbackAssigneeId(agents) : null; const assigneeId = fallbackId ?? assignee!.choice; const alternateAssigneeIds = Object.entries(assignee!.probabilities) @@ -377,9 +353,8 @@ export function routingFromJev(response: JevDecisionResponse, agents: JevAgent[] assigneeId, assigneeSource: fallbackId ? "fallback" : "jev", jevAssigneeId: assignee!.choice, - projectId: project!.choice === JEV_NO_PROJECT ? null : project!.choice, - confidence: { workMode: mode!.confidence, assignee: assignee!.confidence, project: project!.confidence }, - probabilities: { workMode: mode!.probabilities, assignee: assignee!.probabilities, project: project!.probabilities }, + confidence: { workMode: mode!.confidence, assignee: assignee!.confidence }, + probabilities: { workMode: mode!.probabilities, assignee: assignee!.probabilities }, alternateAssigneeIds, usage: response.usage, }; @@ -398,8 +373,6 @@ export function stabilizeRouting(previous: JevRouting | null, next: JevRouting): (next.probabilities[field][nextChoice] ?? 0) - (next.probabilities[field][previousChoice] ?? 0) < JEV_SWITCH_MARGIN; const result: JevRouting = { ...next }; if (keepPrevious("workMode", previous.workMode, next.workMode)) result.workMode = previous.workMode; - const projectKey = (id: string | null) => id ?? JEV_NO_PROJECT; - if (keepPrevious("project", projectKey(previous.projectId), projectKey(next.projectId))) result.projectId = previous.projectId; if (previous.assigneeSource === "jev" && next.assigneeSource === "jev" && keepPrevious("assignee", previous.assigneeId, next.assigneeId)) { result.assigneeId = previous.assigneeId; result.jevAssigneeId = previous.assigneeId; diff --git a/ui/storybook/stories/jev-task-creation.stories.tsx b/ui/storybook/stories/jev-task-creation.stories.tsx index 525efbe2f98..00a89499dd0 100644 --- a/ui/storybook/stories/jev-task-creation.stories.tsx +++ b/ui/storybook/stories/jev-task-creation.stories.tsx @@ -21,7 +21,7 @@ const meta = { description: { component: "Prototype: prompt-first task creation. You write what you need. Jev (`typesafe/jev-1.13`, TypeSafe's decision model on OpenRouter) answers " + - "`choice` questions for the task's mode, which is its type (Auto, Plan, or Ask), plus its assignee and project, each with a confidence and per-option probabilities. Mode, owner, and project update live as you type. Jev doesn't generate text, so a separate small text model drafts the title once, when the task is started with Start task or Enter, like Claude Code naming a session. " + + "`choice` questions for the task's mode, which is its type (Auto, Plan, or Ask), plus its assignee, each with a confidence and per-option probabilities. Mode and owner update live as you type; project is a manual choice. Jev doesn't generate text, so a separate small text model drafts the title once, when the task is started with Start task or Enter, like Claude Code naming a session. " + "Suggested values carry a sparkle. Anything you change is yours and is never overwritten; the undo icon restores the suggestion. " + "Below the confidence threshold the task goes to the org's first active agent that reports to the board (else its first active agent), and Jev's best guesses appear as one-click buttons. **Instant** stories create first; the title and routing arrive a moment later. " + "Jev's answers are simulated locally in the documented response shape (`prototypes/jev-task-creation/jev-classifier.ts`); **How sure Jev is → Request sent to Jev** shows the real request body. No API is called and no task is persisted.",