Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/ai

src/model-step.ts

142 lines · 4.0 KB · TypeScript
import { generateText, type CompleteInput, type ToolCall } from './complete.ts'

export type ModelStepMessage = {
	role: 'system' | 'user' | 'assistant' | 'tool'
	content?: string | null
	tool_calls?: unknown[]
	tool_call_id?: string
	name?: string
}

export type ModelStepInput = CompleteInput & {
	messages: Array<ModelStepMessage>
	conversationId?: string
	useTools?: boolean
}

export type ModelStepResult = {
	ok: boolean
	error?: string
	text: string
	reasoningText: string
	finishReason: string
	toolCalls: Array<ToolCall>
	conversationId: string
	provider?: string
	model?: string
}

/** Tool schemas hosts should expose when they execute Kody search/execute themselves. */
export function kodyAgentToolDefinitions() {
	return [
		{
			type: 'function',
			function: {
				name: 'search',
				description:
					'Discover Kody capabilities, saved packages, persisted values, integrations, and secret metadata (not secret values). Use a natural-language `query` for ranked matches, or pass `entity` (`"{id}:{type}"` or an array of refs) for exact detail/snippet for one or more hits before you execute.',
				parameters: {
					type: 'object',
					properties: {
						query: {
							type: 'string',
							description:
								'Natural language description of what you need, or an exact package UUID / kody id / hosted package URL.',
						},
						entity: {
							description:
								'Exact entity ref like "name:capability", "uuid:package", "name:integration", "user:name:value", or "name:secret", or an array of 1–10 refs.',
							anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
						},
						limit: {
							type: 'number',
							description: 'Max ranked matches (default 15).',
						},
					},
				},
			},
		},
		{
			type: 'function',
			function: {
				name: 'execute',
				description:
					'Run one complete ESM module in the Kody sandbox. Required shape: `export default async function main(input = {}) { ... }`. Prefer `import { kody } from "kody:runtime"` and `kody:@scope/package[/export]` from search/entity detail. Project/slim large payloads before returning. Pass optional `params` as main\'s first argument.',
				parameters: {
					type: 'object',
					properties: {
						code: {
							type: 'string',
							description: 'Full ESM module string with a default export function.',
						},
						params: {
							type: 'object',
							description: 'Optional JSON passed as the first argument to main.',
						},
					},
					required: ['code'],
				},
			},
		},
	]
}

/**
 * One model completion only. Does not execute Kody tools.
 * Host packages should run `kody.search` / `kody.execute` in their own package context.
 */
export async function runModelStep(
	input: ModelStepInput = { messages: [] },
): Promise<ModelStepResult> {
	const conversationId = input.conversationId || crypto.randomUUID()
	try {
		if (!Array.isArray(input.messages) || input.messages.length === 0) {
			throw new Error('messages must include at least one message.')
		}
		const result = await generateText({
			...input,
			tools:
				input.useTools === false
					? undefined
					: Array.isArray(input.tools) && input.tools.length > 0
						? input.tools
						: kodyAgentToolDefinitions(),
			toolChoice: input.useTools === false ? undefined : input.toolChoice ?? 'auto',
		})
		if ('dryRun' in result) {
			return {
				ok: true,
				text: '',
				reasoningText: '',
				finishReason: 'dry_run',
				toolCalls: [],
				conversationId,
				provider: result.provider,
				model: result.model,
			}
		}
		return {
			ok: result.ok,
			error: result.error,
			text: result.text,
			reasoningText: result.reasoningText,
			finishReason: result.finishReason,
			toolCalls: result.toolCalls,
			conversationId,
			provider: result.provider,
			model: result.model,
		}
	} catch (error) {
		return {
			ok: false,
			error: error instanceof Error ? error.message : String(error),
			text: '',
			reasoningText: '',
			finishReason: 'error',
			toolCalls: [],
			conversationId,
		}
	}
}

export default runModelStep