diff --git a/packages/agents/src/agents/zopu.ts b/packages/agents/src/agents/zopu.ts index 757f79f..dbbbd29 100644 --- a/packages/agents/src/agents/zopu.ts +++ b/packages/agents/src/agents/zopu.ts @@ -18,16 +18,8 @@ export default defineAgent(({ env }) => { return { cwd: repositoryRoot, description: "A simple conversational agent with persistent history.", - instructions: `You are Zopu, a concise and helpful assistant. Answer the user's request directly. Ask a clarifying question only when the request cannot be completed without one. - -## Signals -A Signal is your structured interpretation of the user's exact messages into a clear problem statement. Capture them precisely. - -- Call list_signal_source_messages to see admitted user messages not yet used as evidence. -- Do NOT create a Signal for greetings, chitchat, or unclear exploration. Just respond. -- When the user's request is coherent and describes a real problem or goal, compose exactly one Signal: call create_signal once with the ordered messageIds and a structured problem statement {title, summary, desiredOutcome, constraints}. -- Never call create_signal more than once for the same request. It is idempotent on the ordered message selection, but you should still create at most one. -- After creating, report the structured problem statement back to the user (title, summary, desired outcome, constraints) so they can confirm or correct it.`, + instructions: + "You are Zopu, the global planning agent. Help the user clarify and plan work. User messages are durably captured as Signal evidence by the control plane; do not claim that you created a Signal until secure tool execution is wired.", model: `${AGENT_MODEL_PROVIDER}/${AGENT_MODEL_NAME}`, // sandbox: agentos(), sandbox: local({ cwd: repositoryRoot }), diff --git a/packages/agents/src/tools/signals.ts b/packages/agents/src/tools/signals.ts deleted file mode 100644 index 498af7d..0000000 --- a/packages/agents/src/tools/signals.ts +++ /dev/null @@ -1,153 +0,0 @@ -/** - * Zopu Signal tools: candidate-evidence listing and structured Signal creation. - * - * SECURITY MODEL - * -------------- - * Both tools call Convex functions (`signals:listAdmittedUnused`, - * `signals:createFromMessages`) that authorize via the authenticated caller - * identity (`requireOrganizationMember` → `ctx.auth.getUserIdentity()`). They - * therefore require a request-scoped {@link ConvexHttpClient} carrying the - * caller's Bearer JWT — exactly the client `authenticatedAgentRoute` builds - * per request. We never use a shared/global token or a service token here, - * because those carry no user identity and cannot satisfy membership checks. - * - * The factory closes over an authenticated client plus the resolved - * organization (agent instance) id. The agent instance id equals the - * organization id (the hard tenancy boundary enforced by the route), so - * `organizationId` and `conversationId` are never trusted from the agent. - */ -import type { Id } from "@code/backend/convex/_generated/dataModel"; -import { defineTool } from "@flue/runtime"; -import type { ConvexHttpClient } from "convex/browser"; -import { makeFunctionReference } from "convex/server"; -import * as v from "valibot"; - -// --------------------------------------------------------------------------- -// Typed function references (no generated backend bindings imported here, to -// keep the agents package decoupled from backend codegen at type-check time). -// --------------------------------------------------------------------------- - -interface ConversationMessageSourceView { - readonly messageId: string; - readonly clientRequestId: string; - readonly rawText: string; - readonly submissionId: string | null; - readonly createdAt: number; -} - -const listAdmittedUnused = makeFunctionReference< - "query", - { readonly organizationId: Id<"organizations"> }, - ConversationMessageSourceView[] ->("signals:listAdmittedUnused"); - -const createFromMessages = makeFunctionReference< - "mutation", - { - readonly organizationId: Id<"organizations">; - readonly projectId?: Id<"projects">; - readonly conversationId: string; - readonly messageIds: readonly string[]; - readonly problemStatement: { - readonly title: string; - readonly summary: string; - readonly desiredOutcome: string; - readonly constraints: readonly string[]; - }; - readonly processedBy: { - readonly agentName: string; - readonly agentInstanceId: string; - }; - }, - { readonly signalId: Id<"signals"> } ->("signals:createFromMessages"); - -/** - * Input/output schemas shared with the agent instructions. - */ -const problemStatementSchema = v.object({ - title: v.pipe(v.string(), v.maxLength(200)), - summary: v.pipe(v.string(), v.maxLength(2000)), - desiredOutcome: v.pipe(v.string(), v.maxLength(2000)), - constraints: v.pipe(v.array(v.pipe(v.string(), v.maxLength(500))), v.maxLength(20)), -}); - -/** - * Build the Zopu Signal tools bound to one authenticated, organization-scoped - * agent instance. - * - * @param client A request-scoped authenticated Convex client carrying - * the caller's JWT. Built fresh per request; never shared. - * @param organizationId The caller's organization id. Equals the agent - * instance id and the global conversation id. - */ -export const createSignalTools = ( - client: ConvexHttpClient, - organizationId: Id<"organizations"> -) => { - // The conversation id is the organization id (global Zopu conversation). - const conversationId: string = organizationId; - const processedBy = { - agentInstanceId: organizationId, - agentName: "zopu", - }; - - return [ - defineTool({ - description: - "List the admitted user messages in this conversation that have not yet been used as Signal evidence, newest first. Use these messageIds as the ordered source selection when composing a Signal.", - name: "list_signal_source_messages", - output: v.object({ - messages: v.array( - v.object({ - messageId: v.string(), - rawText: v.string(), - createdAt: v.number(), - }) - ), - }), - async run() { - const messages = await client.query(listAdmittedUnused, { - organizationId, - }); - return { - messages: messages.map((message) => ({ - createdAt: message.createdAt, - messageId: message.messageId, - rawText: message.rawText, - })), - }; - }, - }), - defineTool({ - description: - "Create exactly one Signal from an ordered selection of admitted user messages plus a structured problem statement. Idempotent: the same ordered message selection returns the existing Signal. Call this at most once per coherent request, never for greetings or unclear exploration.", - input: v.object({ - messageIds: v.pipe( - v.array(v.string()), - v.minLength(1), - v.maxLength(50) - ), - problemStatement: problemStatementSchema, - projectId: v.optional(v.pipe(v.string(), v.minLength(1))), - }), - name: "create_signal", - output: v.object({ - signalId: v.string(), - }), - async run({ input }) { - const result = await client.mutation(createFromMessages, { - conversationId, - messageIds: [...input.messageIds], - organizationId, - problemStatement: input.problemStatement, - processedBy, - ...(input.projectId === undefined - ? {} - : { projectId: input.projectId as Id<"projects"> }), - }); - return { signalId: result.signalId }; - }, - }), - ]; -};