← Public packages
@kentcdodds/ai
Kody tool-using agent turns with Vercel AI SDK and Cloudflare AI Gateway.
src/index.ts
113 lines · 5.6 KB · TypeScriptimport { runAgentTurn } from './turn.ts'
import type {
AgentChatTurnOutput,
AgentTurnInput,
AgentTurnResult,
AgentTurnStreamEvent,
} from './types.ts'
export { runModelStep, kodyAgentToolDefinitions } from './model-step.ts'
export { projectSearchOutput, projectExecuteOutput } from './turn.ts'
/** Default system guidance for any Kody agent turn (email, chat, automation). */
export const defaultKodyAgentSystem = [
'You are Kody AI: a concise tool-using assistant with two tools, `search` and `execute`.',
'Workflow (generic — apply to any task):',
'1. Prefer `search` first when you need to discover capabilities, saved packages, values, integrations, or secret metadata. Skip search for trivial pure-JS work (dates, formatting, simple transforms) — go straight to `execute`.',
"2. When a search hit looks useful but usage is unclear, call `search` again with `entity` set to that hit's `entityRef` (examples: `integrationList:capability`, `discord:package`, `user:someValue:value`) before inventing APIs.",
'3. When entity detail includes `executeExample`, prefer that module (or a slim projection of its return value) instead of inventing imports. Always `import { kody } from "kody:runtime"` before calling `kody.*`.',
'4. When the plan is clear, call `execute` with one complete ESM module that has `export default async function main(input = {}) { ... }` and returns a slim projected result. Prefer meaningful work per execute over tiny probe loops. On execute errors, read the error and change approach — do not retry the same failing import/code.',
'Search result types matter: `package` = saved package code; `capability` = builtin/runtime API; `value` = persisted config; `integration` / `secret` = auth metadata. Do not call values or channel ids "packages".',
'Inside execute: import helpers from `kody:runtime` (`import { kody } from "kody:runtime"`) and saved packages as `kody:@scope/package` / `kody:@scope/package/export` exactly as search/entity detail documents. Prefer those over raw third-party SDKs.',
'Auth: never ask the user to paste secrets. Use saved integrations/secrets via documented Kody helpers, or report the specific blocker with evidence.',
'Tool calls: use the tool-calling API only. Never write TOOL_CALLS, tool JSON, or API docs as your final answer text.',
'Answers: be brief, grounded in tool evidence, and summarize outcomes (or the blocker) — not raw dumps.',
].join('\n')
function normalizeInput(input: AgentTurnInput = { messages: [] }): AgentTurnInput {
const messages = Array.isArray(input.messages) ? input.messages : []
const system = input.system ?? defaultKodyAgentSystem
return { ...input, messages, system }
}
function toResult(
output: { result?: AgentTurnResult; ok?: boolean; error?: string } | null | undefined,
input: AgentTurnInput,
): AgentTurnResult {
const result = output?.result ?? (output as AgentTurnResult | undefined)
const assistantText =
result?.assistantText ??
result?.text ??
result?.response ??
result?.error ??
(typeof result === 'string' ? result : JSON.stringify(result ?? {}))
return {
assistantText: String(assistantText),
reasoningText: result?.reasoningText ?? '',
summary: result?.summary ?? null,
continueRecommended: Boolean(result?.continueRecommended),
needsUserInput: Boolean(result?.needsUserInput),
stepsUsed: Number(result?.stepsUsed ?? 1),
newInformation: result?.newInformation !== false,
stopReason: result?.stopReason ?? 'completed',
finishReason: result?.finishReason ?? 'stop',
toolCalls: result?.toolCalls ?? [],
conversationId: result?.conversationId ?? input.conversationId ?? crypto.randomUUID(),
}
}
async function runAgentTurnInternal(
input: AgentTurnInput = { messages: [] },
): Promise<AgentChatTurnOutput> {
const normalized = normalizeInput(input)
const output = await runAgentTurn(normalized)
return {
ok: output?.ok !== false,
error: output?.error,
result: toResult(output, normalized),
events: (output?.events ?? []) as AgentTurnStreamEvent[],
}
}
/**
* Run one tool-using agent turn and return the buffered result and event log.
* @param input.messages - At least one user/assistant message; optional `system`, `maxSteps`, `conversationId`.
* @returns `{ ok, result, events }` with assistant text, tool traces, and buffered stream events.
* @example
* import agentChatTurn from 'kody:@kentcdodds/ai'
* const turn = await agentChatTurn({ messages: [{ role: 'user', content: 'What is the weather?' }] })
* // => { ok: true, result: { assistantText: '...', toolCalls: [...] }, events: [...] }
*/
export default async function agentChatTurn(
input: AgentTurnInput = { messages: [] },
): Promise<AgentChatTurnOutput> {
return await runAgentTurnInternal(input)
}
/**
* Async generator that yields buffered turn events after the full turn completes.
* @example
* import { agentTurnStream } from 'kody:@kentcdodds/ai'
* for await (const event of agentTurnStream({ messages: [{ role: 'user', content: 'Hi' }] })) {
* if (event.type === 'assistant_delta') process.stdout.write(event.text)
* if (event.type === 'turn_complete') console.log(event.assistantText)
* }
*/
export async function* agentTurnStream(
input: AgentTurnInput = { messages: [] },
): AsyncGenerator<AgentTurnStreamEvent> {
try {
const output = await runAgentTurnInternal(input)
const text = output.result?.assistantText ?? ''
if (text) yield { type: 'assistant_delta', text }
if (output.result) yield { type: 'turn_complete', ...output.result }
} catch (error) {
yield {
type: 'error',
message: error instanceof Error ? error.message : String(error),
phase: 'run',
}
}
}
export { startAgentTurn, readNextAgentTurnEvents, cancelAgentTurn } from './runs.ts'