fix(agents): avoid advertising unavailable signal tools
This commit is contained in:
@@ -18,16 +18,8 @@ export default defineAgent(({ env }) => {
|
|||||||
return {
|
return {
|
||||||
cwd: repositoryRoot,
|
cwd: repositoryRoot,
|
||||||
description: "A simple conversational agent with persistent history.",
|
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.
|
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.",
|
||||||
## 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.`,
|
|
||||||
model: `${AGENT_MODEL_PROVIDER}/${AGENT_MODEL_NAME}`,
|
model: `${AGENT_MODEL_PROVIDER}/${AGENT_MODEL_NAME}`,
|
||||||
// sandbox: agentos(),
|
// sandbox: agentos(),
|
||||||
sandbox: local({ cwd: repositoryRoot }),
|
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