Skip to Content
Language basics

Language basics

Kora keeps the basics familiar. Start here for values, types, functions, and control flow. Then use the focused guides for Kora’s agent features:

Values and types

Use familiar values, then define a shape when the data deserves a name.

name = "Ada" visits = 3 is_ready = True tags = ["compiler", "coffee"] type Employee: name: str salary: int team: str

kora check verifies local declared assignments and returns, calls, constructors, field access, duplicate definitions, loop control, and direct classified flow into analyze. Values from dynamic integrations stay runtime-checked. Execution commands run this analysis before starting effects.

Functions

def average(xs: list) -> float: return sum(xs) / len(xs) def main(): scores = [8, 9, 10] print(average(scores))

Loops and decisions

for score in scores: if score >= 9: print(f"great: {score}") else: print(f"keep going: {score}")

Lists, dictionaries, attributes, indexing, while, break, continue, and f-strings are all part of the same simple flow.

Handling outcomes

Kora prefers an explicit branch over a surprise exception:

match result: case Ok(value): print(value) case Uncertain(reason): print(f"No clear answer: {reason}")

Context policies

with context(...) keeps tool-loop requests focused without becoming a second spend budget. It estimates input size, reserves space for the answer, and keeps only the newest whole tool exchanges that fit.

with context(max_input_tokens = 12_000, reserve_output_tokens = 2_000): result: Report = analyze(case_file, "review the evidence", tools=[lookup])

Kora never silently trims prompt or data. If the base request cannot fit, the call returns Failed(reason). Tool output remains untrusted data. Its envelope reduces instruction confusion, but it is not a prompt-injection boundary.

The file and the entry point

Kora files end in .ko. A program starts at main() when it is defined.

More than one file

A quoted path imports another Kora file; a bare word is a stdlib module.

use "./lib/tax.ko" as tax use json def main(): print(tax.with_tax(100.0))

The path is resolved against the file that writes it, so a program is a directory you can move whole. Everything the imported file defines at its top level is reachable as tax.name, and that file keeps reading its own names rather than yours.

Packages

A package is a directory with a kora.toml and an entry file. You name it rather than pathing into it:

use pkg receipts as r def main(): print(r.describe("coffee", 4.5))

Where it comes from, and what it may do, live in kora.toml:

[dependencies.receipts] git = "github.com/org/receipts" tag = "v0.3.1" grants = { net = true }

Three things make this different from package managers you have used.

What you declare is not what you download. [dependencies] says where a package comes from; the use pkg statements in your source say whether it is needed at all. Declare a hundred and import four and four are fetched. A package name is always a literal token — Kora has no dynamic import — so this is exact rather than a guess, and a typo’d name never reaches your disk.

A dependency has no ambient authority. It reaches the network, the filesystem, a database, or the environment only where you granted it, and it can never pass on more than it holds. Confinement follows execution, so a package cannot escape it by spawning work or by being called through a tool.

Test-only packages are derived, not declared. There is no [dev-dependencies] table to put something in the wrong half of. A package reached only through test blocks stays out of what ships, and one reached by both a test path and a runtime path is a runtime dependency.

Types belong to their package too, so two dependencies may each declare Config without one becoming the other.

kora add program.ko receipts github.com/org/receipts --tag v0.3.1 kora install program.ko kora tree program.ko

kora.lock pins the commit each reference resolved to, and kora.sums records what that commit contained. Once a repository is locked its commit is what gets fetched, never the tag again — so a tag force-pushed to a backdoor changes nothing about what runs, including on a machine with a cold cache.

Last updated on