fix(agents): avoid advertising unavailable signal tools
This commit is contained in:
@@ -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 }),
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
}),
|
||||
];
|
||||
};
|
||||
Reference in New Issue
Block a user