feat: Slice 1 polish - MiMo V2.5 model, visible reasoning, image attachments, mobile keyboard fix
- Switch conversation agent to xiaomi/mimo-v2.5 (multimodal: text + image) - Render native reasoning parts as live 'Thinking trace' (streaming open, collapsed after completion); inline <think> extraction for streaming models - Image attachments: picker (up to 4, 10MB each), base64 to Flue AgentPromptImage, authenticated blob-URL replay for historical images - Mobile keyboard viewport fix: visual-viewport hook, fixed shell, interactive-widget=resizes-content, header pinned, composer follows keyboard - Conversation to Signal to proposed Work: Convex persistence, Effect validation in @code/work-os, Work cards with exact source provenance - Streamdown markdown + Mermaid chart rendering in chat messages - Flue tool turns hidden, reasoning-containing turns remain visible - Frontend regression tests: keyboard viewport, responsive shell, attachment overflow, authenticated images, reasoning traces, transforms - .env.example updated to xiaomi/mimo-v2.5 config
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
"@code/backend": "workspace:*",
|
||||
"@code/env": "workspace:*",
|
||||
"@code/primitives": "workspace:*",
|
||||
"@code/work-os": "workspace:*",
|
||||
"@flue/runtime": "latest",
|
||||
"@rivet-dev/agentos-core": "catalog:",
|
||||
"convex": "catalog:",
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
import path from "node:path";
|
||||
|
||||
import { parseAgentEnv } from "@code/env/agent";
|
||||
import { defineAgent } from "@flue/runtime";
|
||||
// import { agentos } from "../sandboxes/agentos";
|
||||
import { local } from "@flue/runtime/node";
|
||||
|
||||
import paseo from "../skills/paseo/SKILL.md" with { type: "skill" };
|
||||
import { paseoCli } from "../tools/paseo";
|
||||
import { createSignalRoutingTools } from "../tools/signals";
|
||||
import { createSliceOneTools } from "../tools/slice-one";
|
||||
|
||||
const repositoryRoot = path.resolve(process.cwd(), "../..");
|
||||
|
||||
const INSTRUCTIONS = `You are Zopu, the global planning and work-routing agent for the Zopu Work OS.
|
||||
const INSTRUCTIONS = `You are Zopu for product Slice 1: Conversation to Signal to proposed Work.
|
||||
|
||||
## Your role
|
||||
|
||||
You listen to user messages in the persistent global conversation and decide whether they contain actionable work. The control plane stores every user message as exact evidence before you process it; you never supply or rewrite raw message text.
|
||||
The application stores each user message as exact evidence before you process it. You may interpret that evidence, but never supply, rewrite, or invent source text.
|
||||
|
||||
## Work routing loop
|
||||
|
||||
@@ -23,7 +15,9 @@ When a user sends a message, follow this decision flow:
|
||||
|
||||
1. **Assess actionability.** Does the message contain a concrete problem, request, blocker, opportunity, or decision that warrants a work unit? Greetings, questions about the system, casual conversation, and exploration do NOT create work. If the message is casual conversation, respond naturally and do nothing else.
|
||||
|
||||
2. **Identify project context.** If the message references a project, call list_projects to find it. If the user has one project, use it. If the project is ambiguous, ask one focused clarification.
|
||||
Direct questions, casual conversation, and image-reading requests must be answered immediately without calling any tools.
|
||||
|
||||
2. **Identify project context.** Call list_projects. If there is one project, use it. If there are several and the request is ambiguous, ask one focused question.
|
||||
|
||||
3. **Create a Signal (only when actionable).** When the message is actionable:
|
||||
a. Call list_signal_evidence to see the exact admitted user messages.
|
||||
@@ -31,46 +25,43 @@ When a user sends a message, follow this decision flow:
|
||||
c. Call create_signal with a structured problem statement (title, summary, desiredOutcome, constraints). The problem statement must faithfully represent the user's own intent. Do not invent scope they did not mention.
|
||||
d. Include the projectId when the project is known.
|
||||
|
||||
4. **Route the Signal.** After creating the Signal (or if one already exists):
|
||||
a. Call list_active_issues for the relevant project.
|
||||
b. Compare the Signal's problem to existing issues.
|
||||
c. If the Signal clearly relates to an existing active issue, call attach_signal_to_issue.
|
||||
d. If the Signal is new work that does not match any existing issue, call create_issue_from_signal.
|
||||
4. **Route the Signal.** After creating the Signal:
|
||||
a. Call list_proposed_work for the project.
|
||||
b. If the Signal clearly describes the same desired outcome as existing Work, call attach_signal_to_work.
|
||||
c. Otherwise call create_work_from_signal.
|
||||
e. If genuinely uncertain whether to attach or create, ask one focused question.
|
||||
|
||||
5. **Explain the outcome.** Tell the user clearly what happened:
|
||||
- "Created a Signal: [title] and attached it to issue #[number]: [issue title]."
|
||||
- "Created a Signal: [title] and opened new issue #[number]: [issue title]."
|
||||
- Include the problem statement title so the user can verify accuracy.
|
||||
|
||||
6. **Optionally begin work.** Only when the user explicitly asks to start or work on the issue, call begin_issue. Do not begin work automatically.
|
||||
- "Captured [Signal title] and linked it to [Work title]."
|
||||
- "Captured [Signal title] and proposed [Work title]."
|
||||
- Keep the response brief; the product renders the durable Work card separately.
|
||||
|
||||
## Rules
|
||||
|
||||
- Never supply or rewrite the raw source message text. The control plane copies it server-side.
|
||||
- Never create work from casual chat.
|
||||
- You receive and can see images attached to the current user message. This model supports image input. Never claim images are unavailable, omitted, or unsupported. If a message has attached images, inspect them before responding.
|
||||
- Never create Work from casual chat.
|
||||
- Ask at most one focused clarification when genuinely ambiguous.
|
||||
- Preserve project and organization scope at all times.
|
||||
- Repeated delivery of the same message must not create duplicate Signals or attachments. The backend is idempotent.
|
||||
- Never claim you created a Signal until the tool call returns successfully.
|
||||
- Do not create Candidate Work, Work Units, or Runs directly. Those are downstream concerns.
|
||||
- When the user asks about existing work, use list_active_issues and list_recent_signals to answer.`;
|
||||
- Do not start implementation, planning, sandboxes, Git, verification, or delivery. Those are explicitly outside Slice 1.
|
||||
- Proposed Work is the only Work status in this slice.`;
|
||||
|
||||
export { authenticatedAgentRoute as route } from "../auth";
|
||||
export {
|
||||
authenticatedAgentRoute as attachments,
|
||||
authenticatedAgentRoute as route,
|
||||
} from "../auth";
|
||||
|
||||
export default defineAgent(({ env, id }) => {
|
||||
const { AGENT_MODEL_NAME, AGENT_MODEL_PROVIDER } = parseAgentEnv(env);
|
||||
|
||||
return {
|
||||
cwd: repositoryRoot,
|
||||
description:
|
||||
"Project-scoped work-routing agent that turns conversation into Signals and routes them to issues.",
|
||||
"Turns actionable conversation into provenanced Signals and proposed Work.",
|
||||
instructions: INSTRUCTIONS,
|
||||
model: `${AGENT_MODEL_PROVIDER}/${AGENT_MODEL_NAME}`,
|
||||
// sandbox: agentos(),
|
||||
sandbox: local({ cwd: repositoryRoot }),
|
||||
|
||||
skills: [paseo],
|
||||
tools: [paseoCli, ...createSignalRoutingTools(id, env)],
|
||||
thinkingLevel: "medium",
|
||||
tools: createSliceOneTools(id, env),
|
||||
};
|
||||
});
|
||||
|
||||
145
packages/agents/src/tools/slice-one.ts
Normal file
145
packages/agents/src/tools/slice-one.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { parseAgentEnv } from "@code/env/agent";
|
||||
import { defineTool } from "@flue/runtime";
|
||||
import { ConvexHttpClient } from "convex/browser";
|
||||
import { makeFunctionReference } from "convex/server";
|
||||
import * as v from "valibot";
|
||||
|
||||
const listProjectsRef = makeFunctionReference<
|
||||
"query",
|
||||
{ organizationId: string; token: string },
|
||||
{ _id: string; name: string; description: string | null }[]
|
||||
>("signalRouting:listProjects");
|
||||
|
||||
const listEvidenceRef = makeFunctionReference<
|
||||
"query",
|
||||
{ organizationId: string; token: string },
|
||||
{ messageId: string; rawText: string; createdAt: number }[]
|
||||
>("signalRouting:listEvidence");
|
||||
|
||||
const createSignalRef = makeFunctionReference<
|
||||
"mutation",
|
||||
{
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
messageIds: string[];
|
||||
problemStatement: {
|
||||
title: string;
|
||||
summary: string;
|
||||
desiredOutcome: string;
|
||||
constraints: string[];
|
||||
};
|
||||
processedByAgentInstanceId: string;
|
||||
token: string;
|
||||
},
|
||||
{ signalId: string }
|
||||
>("signalRouting:createSignal");
|
||||
|
||||
const listWorksRef = makeFunctionReference<
|
||||
"query",
|
||||
{ organizationId: string; projectId: string; token: string },
|
||||
{ _id: string; title: string; objective: string; status: "proposed" }[]
|
||||
>("works:listProposedForAgent");
|
||||
|
||||
const createWorkRef = makeFunctionReference<
|
||||
"mutation",
|
||||
{ organizationId: string; signalId: string; token: string },
|
||||
{ created: boolean; workId: string }
|
||||
>("works:createFromSignal");
|
||||
|
||||
const attachSignalRef = makeFunctionReference<
|
||||
"mutation",
|
||||
{ organizationId: string; signalId: string; workId: string; token: string },
|
||||
{ attached: boolean; workId: string }
|
||||
>("works:attachSignalToWork");
|
||||
|
||||
export const createSliceOneTools = (
|
||||
organizationId: string,
|
||||
runtimeEnv: Record<string, string | undefined>
|
||||
) => {
|
||||
const env = parseAgentEnv(runtimeEnv);
|
||||
const client = new ConvexHttpClient(env.CONVEX_URL);
|
||||
const token = env.FLUE_DB_TOKEN;
|
||||
|
||||
return [
|
||||
defineTool({
|
||||
description: "List the projects available in this organization.",
|
||||
name: "list_projects",
|
||||
async run() {
|
||||
return await client.query(listProjectsRef, { organizationId, token });
|
||||
},
|
||||
}),
|
||||
defineTool({
|
||||
description:
|
||||
"List exact admitted user messages that have not yet been used as Signal evidence.",
|
||||
name: "list_signal_evidence",
|
||||
async run() {
|
||||
return await client.query(listEvidenceRef, { organizationId, token });
|
||||
},
|
||||
}),
|
||||
defineTool({
|
||||
description:
|
||||
"Create one structured Signal from exact message IDs. Use only for actionable intent.",
|
||||
input: v.object({
|
||||
messageIds: v.array(v.string()),
|
||||
problemStatement: v.object({
|
||||
constraints: v.array(v.string()),
|
||||
desiredOutcome: v.string(),
|
||||
summary: v.string(),
|
||||
title: v.string(),
|
||||
}),
|
||||
projectId: v.string(),
|
||||
}),
|
||||
name: "create_signal",
|
||||
async run({ input }) {
|
||||
return await client.mutation(createSignalRef, {
|
||||
messageIds: input.messageIds,
|
||||
organizationId,
|
||||
problemStatement: input.problemStatement,
|
||||
processedByAgentInstanceId: organizationId,
|
||||
projectId: input.projectId,
|
||||
token,
|
||||
});
|
||||
},
|
||||
}),
|
||||
defineTool({
|
||||
description:
|
||||
"List proposed Work for a project before deciding whether a new Signal belongs to existing Work.",
|
||||
input: v.object({ projectId: v.string() }),
|
||||
name: "list_proposed_work",
|
||||
async run({ input }) {
|
||||
return await client.query(listWorksRef, {
|
||||
organizationId,
|
||||
projectId: input.projectId,
|
||||
token,
|
||||
});
|
||||
},
|
||||
}),
|
||||
defineTool({
|
||||
description:
|
||||
"Create one proposed Work from a Signal. This is idempotent for the same Signal.",
|
||||
input: v.object({ signalId: v.string() }),
|
||||
name: "create_work_from_signal",
|
||||
async run({ input }) {
|
||||
return await client.mutation(createWorkRef, {
|
||||
organizationId,
|
||||
signalId: input.signalId,
|
||||
token,
|
||||
});
|
||||
},
|
||||
}),
|
||||
defineTool({
|
||||
description:
|
||||
"Attach a Signal to existing proposed Work when it describes the same desired outcome.",
|
||||
input: v.object({ signalId: v.string(), workId: v.string() }),
|
||||
name: "attach_signal_to_work",
|
||||
async run({ input }) {
|
||||
return await client.mutation(attachSignalRef, {
|
||||
organizationId,
|
||||
signalId: input.signalId,
|
||||
token,
|
||||
workId: input.workId,
|
||||
});
|
||||
},
|
||||
}),
|
||||
];
|
||||
};
|
||||
Reference in New Issue
Block a user