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