Tinkuy

@carloscortezcloud/tinkuy-agent

Minimal AI agent framework. ~200 lines. Provider-agnostic. Edge-native. Zero dependencies.

Agent Class

The main class that orchestrates the tool-use loop.

import { Agent } from '@carloscortezcloud/tinkuy-agent';

const agent = new Agent({
  // Required
  router: Router,                 // StyrRouter or any Router-compatible object
  systemPrompt: 'You are...',   // System prompt for the LLM

  // Optional
  tools: Tool[],                  // Array of tools from defineTool()
  guard: Guard,                   // SayayGuard or any Guard-compatible object
  maxIterations: 10,             // Max tool-loop iterations (default 10)
  onIteration: (msg) => void,    // Called after each iteration
  onToolCall: (tool, args) => void, // Called when a tool is invoked
  onComplete: (result) => void,  // Called when agent finishes
});

Methods

agent.run(input)

Run the agent with a prompt. Returns AgentResult.

const result = await agent.run('Hello!');
// → { text, toolsUsed, iterations, latencyMs, costUsd, toolResults }
agent.stream(input)

Stream the agent's response token by token. Returns AsyncGenerator.

for await (const chunk of agent.stream('Hello!')) {
  // chunk: { type: 'token' | 'tool_call' | 'tool_result' | 'done', data }
  process.stdout.write(chunk.data);
}

AgentResult

FieldTypeDescription
textstringFinal response text
toolsUsedstring[]Names of tools called
iterationsnumberLoop iterations
latencyMsnumberTotal time in ms
costUsdnumberEstimated cost in USD
toolResultsobject[]Raw tool execution results

defineTool

Type-safe factory for creating agent tools. Uses JSON Schema for parameter validation.

import { defineTool } from '@carloscortezcloud/tinkuy-agent';

const tool = defineTool({
  name: 'get_weather',                    'Get weather for a city',    // LLM uses this to decide
  parameters: {                              // JSON Schema
    type: 'object',
    properties: {
      city: { type: 'string' },
      units: { type: 'string', enum: ['celsius', 'fahrenheit'] },
    },
    required: ['city'],
  },
  execute: async (args) => {
    return { temperature: 22, condition: 'sunny' };
  },
});

Router Interface

Any object with a call() method works as a router. You can implement your own.

interface Router {
  call(messages: Message[], tools?: Tool[]): Promise<RouterResult>;
}

// Minimal example (wraps raw fetch):
const myRouter: Router = {
  call: async (messages, tools) => {
    const res = await fetch('https://api.openai.com/v1/chat/completions', {
      method: 'POST',
      headers: { Authorization: `Bearer ${process.env.OPENAI_KEY}` },
      body: JSON.stringify({ model: 'gpt-4o-mini', messages }),
    });
    return res.json();
  },
};

Guard Interface

Any object with check() and record() methods works as a guard.

interface Guard {
  check(userId: string, costUsd: number): Promise<GuardDecision>;
  record(userId: string, costUsd: number): Promise<void>;
}

type GuardAction = 'allow' | 'warn' | 'degrade' | 'block';

Hooks

Observability callbacks — no coupling to specific monitoring tools.

onIteration(msg: string) => void

Called after each LLM call with the assistant's response text or tool call info.

onToolCall(tool: string, args: object) => void

Called when a tool is invoked — useful for logging and tracing.

onComplete(result: AgentResult) => void

Called when the agent finishes with the final result (streaming compatible).

Edge Compatibility

Tinkuy has zero dependencies and uses only fetch(). Works everywhere:

Cloudflare WorkersDenoBunNode.js 18+AWS LambdaVercel Edge