@convex-dev/agent
Version:
A agent component for Convex.
514 lines (460 loc) • 14 kB
text/typescript
import { v, type Infer, type Validator, type Value } from "convex/values";
import { vVectorDimension } from "./component/vector/tables.js";
// const deprecated = v.optional(v.any()) as unknown as VNull<unknown, "optional">;
export const vProviderOptions = v.record(
v.string(),
v.record(v.string(), v.any()),
);
const providerOptions = v.optional(vProviderOptions);
export type ProviderOptions = Infer<typeof providerOptions>;
export const vProviderMetadata = vProviderOptions;
const providerMetadata = providerOptions;
export type ProviderMetadata = Infer<typeof providerMetadata>;
export const vThreadStatus = v.union(
v.literal("active"),
v.literal("archived"), // unused
);
export const vMessageStatus = v.union(
v.literal("pending"),
v.literal("success"),
v.literal("failed"),
);
export type MessageStatus = Infer<typeof vMessageStatus>;
export const vRole = v.union(
v.literal("system"),
v.literal("user"),
v.literal("assistant"),
v.literal("tool"),
);
export const vTextPart = v.object({
type: v.literal("text"),
text: v.string(),
providerOptions,
providerMetadata,
});
export const vImagePart = v.object({
type: v.literal("image"),
image: v.union(v.string(), v.bytes()),
mimeType: v.optional(v.string()),
providerOptions,
});
export const vFilePart = v.object({
type: v.literal("file"),
data: v.union(v.string(), v.bytes()),
filename: v.optional(v.string()),
mimeType: v.string(),
providerOptions,
providerMetadata,
});
export const vUserContent = v.union(
v.string(),
v.array(v.union(vTextPart, vImagePart, vFilePart)),
);
export const vReasoningPart = v.object({
type: v.literal("reasoning"),
text: v.string(),
signature: v.optional(v.string()),
providerOptions,
providerMetadata,
});
export const vRedactedReasoningPart = v.object({
type: v.literal("redacted-reasoning"),
data: v.string(),
providerOptions,
providerMetadata,
});
export const vReasoningDetails = v.array(
v.union(
vReasoningPart,
v.object({
type: v.literal("text"),
text: v.string(),
signature: v.optional(v.string()),
}),
v.object({ type: v.literal("redacted"), data: v.string() }),
),
);
export const vSourcePart = v.union(
v.object({
type: v.literal("source"),
sourceType: v.literal("url"),
id: v.string(),
url: v.string(),
title: v.optional(v.string()),
providerOptions,
providerMetadata,
}),
v.object({
type: v.literal("source"),
sourceType: v.literal("document"),
id: v.string(),
mediaType: v.string(),
title: v.string(),
filename: v.optional(v.string()),
providerOptions,
providerMetadata,
}),
);
export type SourcePart = Infer<typeof vSourcePart>;
export const vToolCallPart = v.object({
type: v.literal("tool-call"),
toolCallId: v.string(),
toolName: v.string(),
args: v.any(),
providerExecuted: v.optional(v.boolean()),
providerOptions,
providerMetadata,
});
const vToolResultContent = v.array(
v.union(
v.object({ type: v.literal("text"), text: v.string() }),
v.object({
type: v.literal("image"),
data: v.string(),
mimeType: v.optional(v.string()),
}),
),
);
export const vToolResultOutput = v.union(
v.object({ type: v.literal("text"), value: v.string() }),
v.object({ type: v.literal("json"), value: v.any() }),
v.object({ type: v.literal("error-text"), value: v.string() }),
v.object({ type: v.literal("error-json"), value: v.any() }),
v.object({
type: v.literal("content"),
value: v.array(
v.union(
v.object({ type: v.literal("text"), text: v.string() }),
v.object({
type: v.literal("media"),
data: v.string(),
mediaType: v.string(),
}),
),
),
}),
);
export const vToolResultPart = v.object({
type: v.literal("tool-result"),
toolCallId: v.string(),
toolName: v.string(),
output: v.optional(vToolResultOutput),
providerOptions,
providerMetadata,
providerExecuted: v.optional(v.boolean()),
// Deprecated in ai v5
result: v.optional(v.any()), // either this or output will be present
isError: v.optional(v.boolean()),
// This is only here b/c steps include it in toolResults
// Normal ModelMessage doesn't have this
args: v.optional(v.any()),
experimental_content: v.optional(vToolResultContent),
});
export const vToolContent = v.array(vToolResultPart);
export const vAssistantContent = v.union(
v.string(),
v.array(
v.union(
vTextPart,
vFilePart,
vReasoningPart,
vRedactedReasoningPart,
vToolCallPart,
vToolResultPart,
vSourcePart,
),
),
);
export const vContent = v.union(vUserContent, vAssistantContent, vToolContent);
export type Content = Infer<typeof vContent>;
export const vUserMessage = v.object({
role: v.literal("user"),
content: vUserContent,
providerOptions,
});
export const vAssistantMessage = v.object({
role: v.literal("assistant"),
content: vAssistantContent,
providerOptions,
});
export const vToolMessage = v.object({
role: v.literal("tool"),
content: vToolContent,
providerOptions,
});
export const vSystemMessage = v.object({
role: v.literal("system"),
content: v.string(),
providerOptions,
});
export const vMessage = v.union(
vUserMessage,
vAssistantMessage,
vToolMessage,
vSystemMessage,
);
export type Message = Infer<typeof vMessage>;
export const vSource = v.union(
v.object({
type: v.optional(v.literal("source")),
sourceType: v.literal("url"),
id: v.string(),
url: v.string(),
title: v.optional(v.string()),
providerOptions,
providerMetadata,
}),
v.object({
type: v.literal("source"),
sourceType: v.literal("document"),
id: v.string(),
mediaType: v.string(),
title: v.string(),
filename: v.optional(v.string()),
providerOptions,
providerMetadata,
}),
);
export const vRequest = v.object({
body: v.optional(v.any()),
// These are not usually present
headers: v.optional(v.record(v.string(), v.string())),
method: v.optional(v.string()),
url: v.optional(v.string()),
});
export const vFinishReason = v.union(
v.literal("stop"),
v.literal("length"),
v.literal("content-filter"),
v.literal("tool-calls"),
v.literal("error"),
v.literal("other"),
v.literal("unknown"),
);
export const vUsage = v.object({
promptTokens: v.number(),
completionTokens: v.number(),
totalTokens: v.number(),
reasoningTokens: v.optional(v.number()),
cachedInputTokens: v.optional(v.number()),
});
export type Usage = Infer<typeof vUsage>;
export const vLanguageModelCallWarning = v.union(
v.object({
type: v.literal("unsupported-setting"),
setting: v.string(),
details: v.optional(v.string()),
}),
v.object({
type: v.literal("unsupported-tool"),
tool: v.any(),
details: v.optional(v.string()),
}),
v.object({ type: v.literal("other"), message: v.string() }),
);
export const vMessageWithMetadataInternal = v.object({
message: vMessage,
text: v.optional(v.string()),
fileIds: v.optional(v.array(v.id("files"))),
status: v.optional(vMessageStatus),
// metadata
finishReason: v.optional(vFinishReason),
model: v.optional(v.string()),
provider: v.optional(v.string()),
providerMetadata,
sources: v.optional(v.array(vSource)),
reasoning: v.optional(v.string()),
reasoningDetails: v.optional(vReasoningDetails),
usage: v.optional(vUsage),
warnings: v.optional(v.array(vLanguageModelCallWarning)),
error: v.optional(v.string()),
});
export type MessageWithMetadataInternal = Infer<
typeof vMessageWithMetadataInternal
>;
export const vMessageWithMetadata = v.object({
...vMessageWithMetadataInternal.fields,
fileIds: v.optional(v.array(v.string())),
});
export type MessageWithMetadata = Infer<typeof vMessageWithMetadata>;
export const vMessageEmbeddingsWithDimension = v.object({
model: v.string(),
dimension: vVectorDimension,
vectors: v.array(v.union(v.array(v.number()), v.null())),
});
export type MessageEmbeddingsWithDimension = Infer<
typeof vMessageEmbeddingsWithDimension
>;
export const vMessageEmbeddings = v.object({
model: v.string(),
vectors: v.array(v.union(v.array(v.number()), v.null())),
});
export type MessageEmbeddings = Infer<typeof vMessageEmbeddings>;
export const vContextOptionsSearchOptions = v.object({
limit: v.number(),
textSearch: v.optional(v.boolean()),
vectorSearch: v.optional(v.boolean()),
vectorScoreThreshold: v.optional(v.number()),
messageRange: v.optional(v.object({ before: v.number(), after: v.number() })),
});
export const vContextOptions = v.object({
excludeToolMessages: v.optional(v.boolean()),
recentMessages: v.optional(v.number()),
searchOptions: v.optional(vContextOptionsSearchOptions),
searchOtherThreads: v.optional(v.boolean()),
});
export const vStorageOptions = v.object({
saveMessages: v.optional(
v.union(v.literal("all"), v.literal("none"), v.literal("promptAndOutput")),
),
});
const vPromptFields = {
system: v.optional(v.string()),
prompt: v.optional(v.string()),
messages: v.optional(v.array(vMessage)),
promptMessageId: v.optional(v.string()),
};
export const vCallSettings = v.object({
maxOutputTokens: v.optional(v.number()),
temperature: v.optional(v.number()),
topP: v.optional(v.number()),
topK: v.optional(v.number()),
presencePenalty: v.optional(v.number()),
frequencyPenalty: v.optional(v.number()),
stopSequences: v.optional(v.array(v.string())),
seed: v.optional(v.number()),
maxRetries: v.optional(v.number()),
headers: v.optional(v.record(v.string(), v.string())),
});
export type CallSettings = Infer<typeof vCallSettings>;
const vCommonArgs = {
userId: v.optional(v.string()),
threadId: v.optional(v.string()),
contextOptions: v.optional(vContextOptions),
storageOptions: v.optional(vStorageOptions),
providerOptions,
callSettings: v.optional(vCallSettings),
...vPromptFields,
};
export const vTextArgs = v.object({
...vCommonArgs,
stream: v.optional(v.boolean()),
toolChoice: v.optional(
v.union(
v.literal("auto"),
v.literal("none"),
v.literal("required"),
v.object({ type: v.literal("tool"), toolName: v.string() }),
),
),
maxSteps: v.optional(v.number()),
experimental_continueSteps: v.optional(v.boolean()),
});
export type TextArgs = Infer<typeof vTextArgs>;
export const vSafeObjectArgs = v.object(vCommonArgs);
export type SafeObjectArgs = Infer<typeof vSafeObjectArgs>;
export const vEmbeddingsWithMetadata = v.object({
vectors: v.array(v.union(v.array(v.number()), v.null())),
dimension: vVectorDimension,
model: v.string(),
});
export type EmbeddingsWithMetadata = Infer<typeof vEmbeddingsWithMetadata>;
export function vPaginationResult<
T extends Validator<Value, "required", string>,
>(itemValidator: T) {
return v.object({
page: v.array(itemValidator),
continueCursor: v.string(),
isDone: v.boolean(),
splitCursor: v.optional(v.union(v.string(), v.null())),
pageStatus: v.optional(
v.union(
v.literal("SplitRecommended"),
v.literal("SplitRequired"),
v.null(),
),
),
});
}
export const vStreamCursor = v.object({
streamId: v.string(),
cursor: v.number(),
});
export type StreamCursor = Infer<typeof vStreamCursor>;
export const vStreamArgs = v.optional(
v.union(
v.object({ kind: v.literal("list"), startOrder: v.optional(v.number()) }),
v.object({ kind: v.literal("deltas"), cursors: v.array(vStreamCursor) }),
),
);
export type StreamArgs = Infer<typeof vStreamArgs>;
export const vStreamMessage = v.object({
streamId: v.string(),
status: v.union(
v.literal("streaming"),
v.literal("finished"),
v.literal("aborted"),
),
format: v.optional(
v.union(v.literal("UIMessageChunk"), v.literal("TextStreamPart")),
),
order: v.number(),
stepOrder: v.number(),
// metadata
userId: v.optional(v.string()),
agentName: v.optional(v.string()),
model: v.optional(v.string()),
provider: v.optional(v.string()),
providerOptions: v.optional(vProviderOptions), // Sent to model
});
export type StreamMessage = Infer<typeof vStreamMessage>;
export const vStreamDelta = v.object({
streamId: v.string(),
start: v.number(), // inclusive
end: v.number(), // exclusive
parts: v.array(v.any()),
});
export type StreamDelta = Infer<typeof vStreamDelta>;
export const vMessageDoc = v.object({
_id: v.string(),
_creationTime: v.number(),
userId: v.optional(v.string()), // useful for searching across threads
threadId: v.string(),
order: v.number(),
stepOrder: v.number(),
embeddingId: v.optional(v.string()),
fileIds: v.optional(v.array(v.string())),
error: v.optional(v.string()),
status: vMessageStatus,
// Context on how it was generated
agentName: v.optional(v.string()),
model: v.optional(v.string()),
provider: v.optional(v.string()),
providerOptions: v.optional(vProviderOptions), // Sent to model
// The result
message: v.optional(vMessage),
// Convenience fields extracted from the message
tool: v.boolean(), // either tool call (assistant) or tool result (tool)
text: v.optional(v.string()),
// Result metadata
usage: v.optional(vUsage),
providerMetadata: v.optional(vProviderMetadata), // Received from model
sources: v.optional(v.array(vSource)),
warnings: v.optional(v.array(vLanguageModelCallWarning)),
finishReason: v.optional(vFinishReason),
// Likely deprecated soon
reasoning: v.optional(v.string()),
reasoningDetails: v.optional(vReasoningDetails),
// Deprecated
id: v.optional(v.string()), // external id, e.g. from Vercel AI SDK
});
export type MessageDoc = Infer<typeof vMessageDoc>; // Public
export const vThreadDoc = v.object({
_id: v.string(),
_creationTime: v.number(),
userId: v.optional(v.string()), // Unset for anonymous
title: v.optional(v.string()),
summary: v.optional(v.string()),
status: vThreadStatus,
});
export type ThreadDoc = Infer<typeof vThreadDoc>;