@convex-dev/agent
Version:
A agent component for Convex.
492 lines (441 loc) • 12.5 kB
text/typescript
import {
v,
type Infer,
type ObjectType,
type Validator,
type Value,
type VObject,
} 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,
});
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,
});
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,
});
export const vRedactedReasoningPart = v.object({
type: v.literal("redacted-reasoning"),
data: v.string(),
providerOptions,
});
export const vReasoningDetails = v.array(
v.union(
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 vToolCallPart = v.object({
type: v.literal("tool-call"),
toolCallId: v.string(),
toolName: v.string(),
args: v.any(),
providerOptions,
});
export const vAssistantContent = v.union(
v.string(),
v.array(
v.union(
vTextPart,
vFilePart,
vReasoningPart,
vRedactedReasoningPart,
vToolCallPart
)
)
);
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()),
})
)
);
const vToolResultPart = v.object({
type: v.literal("tool-result"),
toolCallId: v.string(),
toolName: v.string(),
result: v.any(),
// This is only here b/c steps include it in toolResults
// Normal CoreMessage doesn't have this
args: v.optional(v.any()),
experimental_content: v.optional(vToolResultContent),
isError: v.optional(v.boolean()),
providerOptions,
});
export const vToolContent = v.array(vToolResultPart);
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.object({
sourceType: v.literal("url"),
id: v.string(),
url: v.string(),
title: v.optional(v.string()),
providerOptions,
});
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()),
});
const vMessageWithFileAndId = v.object({
id: v.optional(v.string()),
message: vMessage,
fileId: v.optional(v.id("files")),
});
export const vResponse = v.object({
id: v.string(),
timestamp: v.number(),
modelId: v.string(),
headers: v.optional(v.record(v.string(), v.string())), // clear these?
messages: v.array(vMessageWithFileAndId),
body: v.optional(v.any()),
});
export const vResponseWithoutMessages = v.object({
id: v.string(),
timestamp: v.number(),
modelId: v.string(),
headers: v.optional(v.record(v.string(), v.string())), // clear these?
body: v.optional(v.any()),
});
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(),
});
export type Usage = Infer<typeof vUsage>;
export const vLanguageModelV1CallWarning = 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({
id: v.optional(v.string()), // external id, e.g. from Vercel AI SDK
message: vMessage,
text: v.optional(v.string()),
fileIds: v.optional(v.array(v.id("files"))),
// 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(vLanguageModelV1CallWarning)),
error: v.optional(v.string()),
});
export const vMessageWithMetadata = v.object({
...vMessageWithMetadataInternal.fields,
fileIds: v.optional(v.array(v.string())),
});
export type MessageWithMetadata = Infer<typeof vMessageWithMetadata>;
export const vMessageEmbeddings = v.object({
model: v.string(),
dimension: vVectorDimension,
vectors: v.array(v.union(v.array(v.number()), v.null())),
});
export const vObjectResult = v.object({
request: vRequest,
response: vResponseWithoutMessages,
finishReason: vFinishReason,
usage: v.optional(v.any()),
object: v.any(),
error: v.optional(v.string()),
warnings: v.optional(v.array(vLanguageModelV1CallWarning)),
providerMetadata,
});
export type ObjectResult = Infer<typeof vObjectResult>;
export const vSearchOptions = v.object({
vector: v.optional(v.array(v.number())),
vectorModel: v.optional(v.string()),
text: v.optional(v.string()),
limit: v.number(),
vectorScoreThreshold: v.optional(v.number()),
messageRange: v.optional(v.object({ before: v.number(), after: v.number() })),
});
export type SearchOptions = Infer<typeof vSearchOptions>;
export const vContextOptionsSearchOptions = v.object({
limit: v.number(),
textSearch: v.optional(v.boolean()),
vectorSearch: v.optional(v.boolean()),
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 vStorageOptionsIncludingDeprecated = v.object({
...vStorageOptions.fields,
saveAllInputMessages: v.optional(v.boolean()),
saveAnyInputMessages: v.optional(v.boolean()),
saveOutputMessages: v.optional(v.boolean()),
}) as VObject<
{
saveMessages?: "all" | "none" | "promptAndOutput";
/**
* @deprecated Use saveMessages instead.
*/
saveAllInputMessages?: boolean;
/**
* @deprecated Use saveMessages instead.
*/
saveAnyInputMessages?: boolean;
/**
* @deprecated Use saveMessages instead.
*/
saveOutputMessages?: boolean;
},
typeof vStorageOptions.fields
>;
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 vCallSettingsFields = {
maxTokens: 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()),
seed: v.optional(v.number()),
maxRetries: v.optional(v.number()),
headers: v.optional(v.record(v.string(), v.string())),
};
export type CallSettings = ObjectType<typeof vCallSettingsFields>;
const vCommonArgs = {
userId: v.optional(v.string()),
threadId: v.optional(v.string()),
contextOptions: v.optional(vContextOptions),
storageOptions: v.optional(vStorageOptionsIncludingDeprecated),
providerOptions,
...vCallSettingsFields,
...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 vTextStreamPart = v.union(
v.object({
type: v.literal("text-delta"),
textDelta: v.string(),
}),
v.object({
type: v.literal("reasoning"),
textDelta: v.string(),
}),
v.object({
type: v.literal("source"),
source: vSource,
}),
vToolCallPart,
v.object({
type: v.literal("tool-call-streaming-start"),
toolCallId: v.string(),
toolName: v.string(),
}),
v.object({
type: v.literal("tool-call-delta"),
toolCallId: v.string(),
toolName: v.string(),
argsTextDelta: v.string(),
}),
vToolResultPart
);
export type TextStreamPart = Infer<typeof vTextStreamPart>;
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"),
}),
v.object({
kind: v.literal("deltas"),
cursors: v.array(vStreamCursor),
})
)
);
export type StreamArgs = Infer<typeof vStreamArgs>;
export const vStreamMessage = v.object({
streamId: v.string(),
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(vTextStreamPart),
});
export type StreamDelta = Infer<typeof vStreamDelta>;