feat(agents): add signal tools to zopu agent

This commit is contained in:
-Puter
2026-07-23 17:16:59 +05:30
parent 6d86c8e538
commit d3381f978f
3 changed files with 244 additions and 2 deletions

View File

@@ -53,6 +53,19 @@ interface ComposedSignal {
readonly sources: readonly SignalSourceView[];
}
/**
* An admitted user conversation message presented to the agent as candidate
* Signal evidence. The agent selects from these by `messageId`; raw text and
* timestamps are copied server-side and never accepted from the agent.
*/
interface ConversationMessageSourceView {
readonly messageId: string;
readonly clientRequestId: string;
readonly rawText: string;
readonly submissionId: string | null;
readonly createdAt: number;
}
const toSignalView = (doc: Doc<"signals">): SignalView => ({
_creationTime: doc._creationTime,
_id: doc._id,
@@ -78,6 +91,16 @@ const toSourceView = (doc: Doc<"signalSources">): SignalSourceView => ({
submissionId: doc.submissionId ?? null,
});
const toConversationMessageSourceView = (
doc: Doc<"conversationMessages">
): ConversationMessageSourceView => ({
clientRequestId: doc.clientRequestId,
createdAt: doc.createdAt,
messageId: doc.messageId,
rawText: doc.rawText,
submissionId: doc.submissionId ?? null,
});
/**
* Build a deterministic, collision-safe key from the organization, the
* conversation, and the ordered message selection. The key is the JSON
@@ -348,6 +371,64 @@ export const list = query({
},
});
/**
* List the bounded set of admitted user messages in the organization's
* conversation that have not yet been consumed as a Signal source, newest
* first. These are the candidate messages an agent should select from when
* composing a new Signal.
*
* Authorization: the authenticated identity must be a member of the target
* organization. The conversation id equals the organization id; the caller
* never supplies tenancy. Cross-organization access is denied.
*
* Bounded: capped at 100 rows to keep agent evidence selection tractable.
*/
export const listAdmittedUnused = query({
args: {
organizationId: v.id("organizations"),
},
handler: async (
ctx,
args
): Promise<ConversationMessageSourceView[]> => {
await requireOrganizationMember(ctx, args.organizationId);
const conversationId = args.organizationId;
const admitted = await ctx.db
.query("conversationMessages")
.withIndex("by_organization_and_message", (q) =>
q
.eq("organizationId", args.organizationId)
.eq("conversationId", conversationId)
)
.order("desc")
.take(100);
const candidates = admitted.filter(
(message) =>
message.role === "user" && message.status === "admitted"
);
const unused: ConversationMessageSourceView[] = [];
for (const message of candidates) {
const consumed = await ctx.db
.query("signalSources")
.withIndex("by_organization_and_message", (q) =>
q
.eq("organizationId", args.organizationId)
.eq("conversationId", conversationId)
.eq("messageId", message.messageId)
)
.first();
if (!consumed) {
unused.push(toConversationMessageSourceView(message));
}
}
return unused;
},
});
/**
* Get one Signal by id with its composed sources. Membership in the signal's
* organization is required; access to another organization's signal is denied.