kortyx
v0.25.0
Published
TypeScript framework for production AI agents with explicit workflows, provider-agnostic models, streaming, interrupts, and runtime persistence.
Maintainers
Readme
kortyx
Kortyx is a TypeScript framework for building production AI agents with explicit workflows, provider-agnostic models, typed hooks, streaming, interrupts, and runtime persistence.
Use kortyx as the main package. It re-exports the public server/runtime APIs from the supporting @kortyx/* packages so application code can stay focused on workflows, nodes, providers, and UI transport.
Install
pnpm add kortyx @kortyx/google @kortyx/reactnpm install kortyx @kortyx/google @kortyx/reactRun Studio locally
Kortyx includes the local Studio command. With Docker Desktop running:
npx kortyx studio startThis starts the self-hosted Studio, API, and Postgres stack, then prints the sign-in and server-side telemetry variables for this SDK project. Re-running the command is safe and preserves credentials and data.
Studio is a source-available preview under Elastic License 2.0; the Kortyx framework and CLI remain Apache-2.0. See the self-hosted preview guide for backup, upgrade, security, and limitation details.
npx kortyx studio status
npx kortyx studio logs
npx kortyx studio credentials
npx kortyx studio credentials --rotate
npx kortyx studio stopFor an operator-managed installation with external PostgreSQL, follow the server deployment guide.
Quickstart
Create a workflow:
// src/workflows/general-chat.workflow.ts
import { defineWorkflow } from "kortyx";
import { chatNode } from "@/nodes/chat.node";
export const generalChatWorkflow = defineWorkflow({
id: "general-chat",
version: "1.0.0",
description: "Single-node chat workflow.",
nodes: {
chat: {
run: chatNode,
params: {
temperature: 0.3,
},
},
},
edges: [
["__start__", "chat"],
["chat", "__end__"],
],
});Add a server-side node:
// src/nodes/chat.node.ts
import { google } from "@kortyx/google";
import { useReason } from "kortyx";
type ChatParams = {
temperature?: number;
};
export const chatNode = async ({
input,
params,
}: {
input: unknown;
params: ChatParams;
}) => {
const result = await useReason({
id: "chat",
model: google("gemini-2.5-flash"),
system: "You are a concise assistant.",
input: String(input ?? ""),
temperature: params.temperature ?? 0.3,
stream: true,
emit: true,
});
return {
data: { text: result.text },
};
};Wire the agent:
// src/lib/agent.ts
import { createAgent } from "kortyx";
import { generalChatWorkflow } from "@/workflows/general-chat.workflow";
export const agent = createAgent({
workflows: [generalChatWorkflow],
defaultWorkflowId: "general-chat",
});Expose it through a Next.js API route:
import { createChatRouteHandler } from "kortyx";
import { agent } from "@/lib/agent";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
const handleChat = createChatRouteHandler({ agent });
export async function POST(request: Request): Promise<Response> {
return handleChat(request);
}Consume the stream from React:
"use client";
import { createRouteChatTransport, useChat } from "@kortyx/react";
export function Chat() {
const chat = useChat({
transport: createRouteChatTransport({ endpoint: "/api/chat" }),
});
return (
<form
onSubmit={(event) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
chat.send(String(form.get("message") ?? ""));
event.currentTarget.reset();
}}
>
{chat.messages.map((message) => (
<p key={message.id}>{message.content}</p>
))}
{chat.streamContentPieces.map((piece) =>
piece.type === "text" ? (
<span key={piece.id}>{piece.content}</span>
) : null,
)}
<input name="message" />
<button type="submit" disabled={chat.isStreaming}>
Send
</button>
</form>
);
}Set a provider key and run your app:
GOOGLE_API_KEY=your_key_here pnpm devAPI Groups
| Area | Exports |
| --- | --- |
| Agent | createAgent, createChatRouteHandler, streamChatFromRoute |
| Workflows | defineWorkflow, loadWorkflow, validateWorkflow |
| Hooks | useReason, useInterrupt, useStructuredData, useNodeState, useWorkflowState, useRuntimeContext |
| Runtime | workflow registries, node registry, in-memory/Redis framework adapters |
| Providers | provider contracts and registry helpers from @kortyx/providers |
| Streams | SSE helpers, stream readers, collectors, structured reducers |
Provider Packages
Install only the provider integrations your app needs.
| Provider | Package | Factory | Default environment variable |
| --- | --- | --- | --- |
| Google Gemini | @kortyx/google | google(...) | GOOGLE_API_KEY or GEMINI_API_KEY |
| OpenAI | @kortyx/openai | openai(...) | OPENAI_API_KEY |
| Anthropic | @kortyx/anthropic | anthropic(...) | ANTHROPIC_API_KEY |
| DeepSeek | @kortyx/deepseek | deepseek(...) | DEEPSEEK_API_KEY |
| Groq | @kortyx/groq | groq(...) | GROQ_API_KEY |
| Mistral | @kortyx/mistral | mistral(...) | MISTRAL_API_KEY |
Documentation
Studio
The self-hosted Kortyx Studio preview provides run, session, workflow, interrupt, payload, timing, token, and cost inspection for telemetry emitted by Kortyx SDK applications.
Telemetry
Configure server-side Studio telemetry with createKortyxTelemetryAdapter:
import { createAgent } from "kortyx";
import { createKortyxTelemetryAdapter } from "@kortyx/telemetry";
const telemetry = createKortyxTelemetryAdapter({
endpoint: process.env.KORTYX_TELEMETRY_API_URL!,
apiKey: process.env.KORTYX_TELEMETRY_API_KEY!,
environment:
process.env.KORTYX_TELEMETRY_ENVIRONMENT ?? "development",
service: {
name: process.env.KORTYX_TELEMETRY_SERVICE_NAME ?? "support-api",
deploymentRef: process.env.GIT_SHA,
},
maxQueueSize: 1_000,
});
const agent = createAgent({ workflows, telemetry });Delivery is best-effort and non-blocking: events receive idempotent IDs, are batched in a bounded in-memory queue, and transient network/429/5xx failures retry with exponential backoff and jitter. Use telemetry.flush() during graceful shutdown; inspect getDroppedEventCount() and getPermanentDeliveryFailureCount() for delivery health. Prompt and output content is excluded by default. Enable only the sides you intend to persist with captureContent: true or { input: true, output: true }.
interrupt.expired is intentionally API-derived from the durable expiresAt sent in interrupt.created; the SDK does not run an unreliable local TTL timer. run.cancelled records aborted active executions. Forward request.signal in custom routes; createChatRouteHandler forwards it automatically. Children, models and tools inherit the live signal. Use useAbortSignal() for custom node I/O.
Applications that own visible conversation history can use
createChatRouteHandler({ onTurnAccepted, onResponseFinalized }) to write a
pending user turn before execution and a parsed assistant message after the
response's checkpoint decision. The hooks require sessionId and
clientTurnId; @kortyx/react sends the latter from the user message ID.
onResponseFinalized includes status, checkpointId, and a message with
ordered text, structured, interrupt, and error pieces. Configure
disconnect: "continue" with onExecution to finish an ordinary run after
the browser closes its stream. The default remains request cancellation.
createCheckpointRouteHandler additionally exposes onForked and
onRolledBack for app-owned transcript updates. Hook delivery is in-process;
crash recovery and cross-store transactions remain application concerns. See
Background Continuation.
Interrupt telemetry keeps structural fields (kind, interactionMode,
optionCount, schemaId, and schemaVersion) even when content capture is
off. Questions and static option labels use output-content capture; submitted
responses use input-content capture. Option values and resume capability tokens
are never emitted.
License
Apache-2.0. See LICENSE.
Error handling
Use serializeFailure for safe structured diagnostics and DomainError for explicitly approved domain details that must survive child checkpoint restore. Cancellation, execution limits and human interrupts remain control flow. Applications own retry and schema correction policy. See the error handling guide.
Throw ordinary Error instances when a workflow cannot continue; configured tracing records them automatically before the run fails. Use reportError(error, {severity?, metadata?, tags?}) only for handled errors when application code deliberately continues with a fallback. Reporting is best-effort observation and never changes workflow control flow.
Shared tools
await useTool({tool, input}) executes a typed plain tool immediately inside a workflow node, without a model call or MCP transport. Pass the same definition to useReason({tools: [tool]}) for model-driven selection and arguments. Tool definitions can provide outcomes.denialCodes, classifyResult and classifyError; permission enforcement remains application-owned. Direct calls preserve result/error identity and do not cache executions.
Studio records individual durations and separate success, denial, fault and cancellation outcomes; cached native and child calls appear as reuse with their original scope. Tool faults automatically include error type and message (up to 256/8192 characters), without extra call-site wiring. Observations exclude raw inputs/results, exception objects, causes and stacks. Error messages are exported verbatim and can contain sensitive text; an optional tool telemetry.error(error) callback can replace them with {type, message} or return null to suppress them. Throwing projections suppress diagnostics and never alter execution. Observer and delivery failures cannot change execution. kortyx topology push discovers attached tools from source, marks dynamic attachments unresolved, and makes available tools visible before traffic.
Configured Studio/OpenTelemetry tracing captures thrown workflow/model errors automatically, including bounded type, message, stack and cause details. Studio also records manual reportError calls as handled errors without failing the span. Prompts, outputs and raw provider responses remain excluded by default. Diagnostics are verbatim and may contain sensitive text; the optional trace adapter error(error) override can return {type, message, stack?, cause?} or null to replace or suppress them without changing execution. Client-facing failure contracts remain sanitized and unchanged.
