Skip to main content

Package System

AILANG's package system enables multi-package projects with deterministic dependency resolution, export enforcement, and effect ceilings. Packages are the primary unit of autonomous coordination — they define what an agent can see, change, what authority it has, and what guarantees it must preserve.

Published package pages also snapshot and display the package's AGENT.md guide as raw text, so consumers can review machine-facing usage instructions before installing.

Quick Start​

# Create a package
ailang init package --name myorg/mylib
ailang init package --name sunholo/myapp --module-prefix myapp # for existing apps

# Add a dependency
ailang add --path ../shared-utils

# Resolve dependencies and generate lock file
ailang lock

# Validate package (cross-module type checking)
ailang check --package .

# Run tests
ailang test --package .

# View dependency tree
ailang tree

# Publish to registry
ailang publish --dry-run
ailang publish

# Run (package resolution is automatic)
ailang run main.ail

Package Structure​

A package is defined by an ailang.toml manifest:

my-package/
ailang.toml # Package manifest (human-written)
ailang.lock # Resolved dependencies (machine-generated)
src/
core.ail # Source modules
helpers.ail
tests/
core_test.ail

Manifest (ailang.toml)​

[package]
name = "sunholo/docparse"
version = "0.3.0"
edition = "1"
ailang = ">=0.9.5"
description = "Parse documents into structured blocks"
license = "MIT"

[exports]
modules = [
"sunholo/docparse/parser",
"sunholo/docparse/types"
]

[dependencies]
"sunholo/json" = { path = "../json" }

[effects]
max = ["IO", "FS"]

[stability]
level = "experimental"

Required Fields​

  • [package].name — Two-level vendor/name format
  • [package].version — Semantic version
  • [package].edition — Language edition (currently "1")

Optional Fields​

  • [package].ailang — Minimum AILANG version required (e.g., ">=0.9.5"). Auto-set by ailang init package. Enforced on ailang install — incompatible packages are rejected with a clear error message.
  • [package].module_prefix — Maps existing module paths to the package namespace without renaming source files. See Module Prefix below.

Module Prefix​

Existing applications can adopt the package system without renaming all module declarations. If your project uses module myapp/services/api but you want to publish as sunholo/myapp, set module_prefix:

[package]
name = "sunholo/myapp"
module_prefix = "myapp"

[exports]
modules = ["myapp/services/api", "myapp/handlers/parse"]

Source files keep their existing module myapp/... declarations. Consumers import via the canonical path:

import pkg/sunholo/myapp/services/api (startServer)

The loader maps sunholo/myapp/services/api → myapp/services/api automatically.

Rules:

  • module_prefix must be a single segment (no slashes)
  • Exports can use either module_prefix/... or vendor/name/... prefixes
  • CLI: ailang init package --name sunholo/myapp --module-prefix myapp

Exports​

Only modules listed in [exports].modules are accessible to dependents. Modules not in this list are package-private.

[exports]
modules = ["sunholo/docparse/parser"]
# sunholo/docparse/internal/helpers is NOT accessible from outside

Effect Ceiling​

Packages declare their maximum allowed effects:

[effects]
max = ["IO"] # Only IO allowed; Net, FS, etc. would be a compile error

The ceiling covers what the package's own code can reach. The package's own modules are its local modules and its siblings, including any imported as ./x or pkg/<self>/x. For each own module, compilation fails if either of these includes an effect not in max:

  • the effects a function declares in its signature;
  • the effects in the type of any reference to a function outside the package: a stdlib or dependency function, or a builtin. The reference counts wherever it appears — called, passed to a higher-order function, stored in a record, or returned inside a closure, even one that is never called.
effect ceiling violation in package sunholo/mylib: effects [FS] not in max [IO]
Add missing effects to [effects].max in ailang.toml

Imported modules' own bodies are not charged. Importing only disconnect from std/stream needs Stream, not the Process that std/stream.asyncExecProcess uses. An imported name that is never referenced charges nothing. The effect rows that appear only in the types of your own values are not charged either. For example, a hook must carry an ABI's fixed row even when its body uses less. Performing such a callback still requires declaring its effects.

An empty max = [] means the package is pure — no effects allowed.

Ghost effects (like Debug) are exempt from ceiling checks. Packages can use std/debug or sunholo/logging without listing Debug in [effects].max. Ghost effects are invisible to callers, auto-granted at runtime, and erased in --release mode.

Stability​

[stability]
level = "experimental" # or "stable" | "frozen"

Commands ([bin])​

A package can ship commands as well as modules. Each [bin] entry names a module whose exported entry function becomes a CLI; ailang install puts a shim for it on PATH, so a consumer gets the command with one command — no clone, no wrapper script.

[bin]
eparse = "cli" # module sunholo/email/cli, entry main, caps auto
docparse = { module = "docparse/main", # module_prefix layouts work too
entry = "main",
caps = "IO,FS,Env,AI", # pin caps instead of inferring them
run_flags = ["--max-recursion-depth", "50000", "--process-timeout", "5m"] }
FieldDefaultMeaning
modulerequiredResolved like an import: cli, vendor/name/cli and src/cli.ail all name the same file
entrymainExported function to run
capsauto--caps for the run; auto infers them from the entry's declared effects
run_flags[]Extra ailang run flags, verbatim. --caps, --entry, --package-dir and --quiet are the shim's and are rejected here

The command receives its positional arguments through std/env getArgs(); arguments that look like flags (eparse --triage 5) reach the program untouched.

ailang install sunholo/email # library + `eparse` on PATH
ailang install --path packages/email # the developer loop: shim this checkout, no publish
ailang install --no-bin sunholo/email # library only
ailang bin list # what is installed, from which package@version
ailang bin uninstall eparse

Shims live in ~/.ailang/bin (--bin-dir overrides); install prints the PATH line to add when the directory is not on it, and warns when another eparse earlier on PATH would shadow the shim. A shim is a five-line sh file that execs ailang run --quiet --package-dir <pkg> --entry <entry> --caps <caps> <file> -- "$@": the installed package directory is the program root, and ailang install writes the package's ailang.lock beside its cached manifest so its own dependencies resolve from any working directory. ailang publish refuses a [bin] whose module does not exist or whose entry is not exported.

Lock File (ailang.lock)​

Generated by ailang lock. Committed to version control. Contains:

  • Content hash (SHA256 of all .ail source files)
  • Interface hash (SHA256 of exports + effects — stays same on internal refactor)
  • Resolved dependency paths

The lock file is validated at build time — if a dependency's content has changed, you'll see:

Warning: dependency sunholo/json content changed (locked: sha256:a1b2c3... current: sha256:d4e5f6...)
Run 'ailang lock' to update

Importing from Packages​

Use the pkg/ prefix for external package imports:

-- Stdlib (always available)
import std/io (println)

-- Local module (same project)
import myproject/parser (parse)

-- External package
import pkg/sunholo/json/parser (parseJson)

The pkg/ prefix tells the compiler to resolve against the lock file instead of the local filesystem.

Dependencies​

Path Dependencies​

Link to local packages:

[dependencies]
"shared/utils" = { path = "../utils" }
"shared/types" = { path = "../types" }

Git Dependencies​

Pin to a specific tag or commit in a git repository:

[dependencies]
"sunholo/auth" = { git = "https://github.com/sunholo-data/ailang-packages", subdir = "packages/auth", tag = "main" }

Registry Dependencies​

Install from the AILANG package registry:

[dependencies]
"sunholo/auth" = "0.1.0"

Resolution priority: path > git > registry.

CLI Commands​

CommandDescription
ailang init package --name vendor/nameCreate ailang.toml
ailang add --path ../depAdd path dependency
ailang add --git URL --subdir DIR --tag TAGAdd git dependency
ailang install vendor/nameInstall latest from registry
ailang install vendor/name@versionInstall exact version from registry
ailang install --path DIRShim a local package's [bin] commands onto PATH
ailang bin list / ailang bin uninstall NAMEManage installed [bin] shims
ailang lockResolve deps, generate ailang.lock
ailang treeDisplay dependency tree
ailang search "query"Search registry packages
ailang publishPublish to registry
ailang pkg-docs vendor/nameView package AGENT.md

Registry​

The AILANG package registry is hosted on GCP at https://storage.googleapis.com/ailang-registry.

Publishing​

cd my-package/
ailang pkg quality . # The report publish and the validator both run (v0.40.0)
ailang publish --dry-run # Same report + tarball preview; exits non-zero on any PUB gate
ailang publish # Upload to registry (requires AILANG_REGISTRY_API_KEY)

publish runs the quality report first and refuses on the same PUBnnn codes the validator would return. The validator then recomputes the server sections itself:

  • Compiles your package (ailang check --package)
  • Verifies effect ceilings match [effects].max
  • Verifies contracts package-wide (ailang verify --package) and banks the counts
  • Builds the signature-sensitive interface identity (v2) and refuses publisher/validator skew
  • Requires a ## <version> section in CHANGELOG.md and a [release] kind (gates from v0.41.0)
  • Computes content + interface + tarball hashes

Tests and _smoke.ail run only on your machine and travel as an attested block the registry displays but never gates on. Details: the package publishing guide.

  • Rejects duplicate versions (immutable once published)

Searching​

ailang search "auth" # Keyword search on name, ai_summary, tags
ailang search --tag gcp # Filter by tag
ailang search # List all packages

Installing​

ailang install sunholo/auth # Install latest version
ailang install sunholo/auth@latest # Same as above
ailang install sunholo/auth@0.1.0 # Install exact version

The resolved exact version is always written to ailang.toml. Semver ranges (^, ~, >=) are not supported — AILANG requires exact versions for deterministic builds.

Lock File Portability​

ailang.lock is portable across machines and containers — it does not contain absolute paths. Cache paths for registry and git packages are resolved at runtime. This means you can COPY ailang.lock into Docker and run ailang install to populate the cache.

Path dependencies ({ path = "../sim" }) are recorded relative to the depending package's ailang.toml, with forward slashes on every OS, and their content hash covers only the files' relative names and contents. A repo holding several packages (say sim/ and ai/, with ai depending on ../sim) can commit ai/ailang.lock and it stays valid in a CI clone or a copy unpacked anywhere else.

Run ailang lock --check in CI to verify the committed lock is current without rewriting it; it exits non-zero and lists each drifted entry. A lock written by an older ailang that names a path dependency by absolute path still loads while that directory exists, is reported as drift by --check, and is rewritten relative by the next ailang lock. If the absolute directory is gone (another checkout), the error says so and tells you to run ailang lock.

On a fresh machine, run ailang lock from the package directory to resolve every dependency in the committed manifest and populate the package cache. ailang install <package> adds a named dependency; it is not the restore command for an existing manifest. Resolution still runs when the lock is current so missing cache entries can be fetched.

An unchanged resolution preserves generated_at, generator, and ailang_version together, leaving a CLI-generated lock byte-identical even after a toolchain upgrade. Changed resolutions receive fresh metadata. CI can therefore use either ailang lock --check (read-only drift detection) or ailang lock followed by git diff --exit-code -- ailang.lock; timestamp-ignore filters and restoring the file are unnecessary. Malformed or unreadable existing locks produce an error.

Version Conflict Detection​

AILANG uses flat dependencies — one version per package name. If a transitive dependency requires a different version than your root manifest pins, ailang lock fails with a structured error:

version conflict: sunholo/firestore
root requires: 0.2.0
already resolved: 0.1.0
transitive requires: 0.1.0 (via sunholo/billing_store)

resolution aborted

suggestion:
- republish sunholo/billing_store against sunholo/firestore@0.2.0
- or change root dependency to sunholo/firestore@0.1.0 explicitly

Direct dependencies are authoritative — the resolver never silently downgrades them. To fix a conflict, either republish the transitive package or change your root pin to match.

Package Documentation​

ailang pkg-docs sunholo/auth # Display AGENT.md usage guide

Each package can include an AGENT.md — a structured guide for AI agents with quick start, exported functions, and common patterns.

Environment Variables​

VariablePurpose
AILANG_REGISTRYRegistry URL (default: https://storage.googleapis.com/ailang-registry)
AILANG_REGISTRY_VALIDATORValidator service URL (for ailang publish)
AILANG_REGISTRY_API_KEYAPI key for publishing
AILANG_CLOUD_PROJECTGCP project hosting the cascade Pub/Sub topic (e.g. ailang-multivac-dev). When unset, ailang publish skips the cascade-topic publish and only emits legacy inbox messages.
AILANG_TOPIC_PREFIXPrefix for cascade topic names (default: ailang). Cloud envs use the env-specific prefix (ailang-dev, ailang-test, ailang). The full topic name is <prefix>-cascade.

Cascade env vars: Both AILANG_CLOUD_PROJECT and AILANG_TOPIC_PREFIX must match the cloud coordinator's deployment. For the public ailang-dev environment:

export AILANG_CLOUD_PROJECT=ailang-multivac-dev
export AILANG_TOPIC_PREFIX=ailang-dev

Without these, ailang publish still uploads to the registry and notifies dependent inboxes, but the cloud coordinator never wakes up to open repair PRs — the cascade is effectively local-only.

Admin: Rebuild Index​

If the registry index becomes corrupted, an admin can rebuild it from all published metadata:

curl -X POST https://<validator-url>/rebuild-index \
-H "X-API-Key: $AILANG_REGISTRY_API_KEY"

This scans all metadata.json files in the bucket and reconstructs index.json. Same API key auth as publish.

Available Packages​

PackageDescriptionEffects
sunholo/authAPI key validation, HMAC hashing, bearer tokensPure
sunholo/gcp_authGCP ADC OAuth2 token exchange, project detectionFS, Net, Env
sunholo/http_helpersHTTP request builders, auth headers, JSON response parsingNet
sunholo/loggingStructured JSON logging for Cloud RunIO
sunholo/configConfig loading from env vars with validationEnv
sunholo/testing_utilsTest assertion helpersPure
sunholo/registry_validatorPackage validator written in AILANG (dogfooding)IO, FS

Source: github.com/sunholo-data/ailang-packages

Dual Hash Model​

Each package in the lock file has two hashes:

HashChanges WhenPurpose
Content hashAny source file changesReproducibility
Interface hashExports or effects changeCoordination — internal refactors don't cascade

Backward Compatibility​

Projects without ailang.toml work exactly as before. The package system is opt-in — existing module imports (std/..., local modules) are unaffected.

Workspace Pattern​

Multi-package repos use path dependencies without needing a registry:

workspace/
types/
ailang.toml # name = "myorg/types"
parser/
ailang.toml # name = "myorg/parser", depends on types via path
app/
ailang.toml # depends on types + parser via path

Each package is independently valid. Workspaces are emergent from path-linked packages, not a separate manifest type.

AI Agent Coordination​

The registry is more than a package manager — it's the coordination layer through which AI agents discover, share, and compose capabilities.

Why This Matters for Multi-Agent Workflows​

When multiple AI agents work on a project (or across projects), the registry provides the shared vocabulary:

  • Effect ceilings — An agent can inspect [effects].max to know exactly what authority a package has. A package with max = ["IO"] cannot access the network, filesystem, or environment. This lets agents reason about trust boundaries without reading source code.

  • Interface hashes — The interface hash changes only when exports or effects change. An internal refactor (same exports, same effects) produces a new content hash but the same interface hash. Downstream agents can skip re-verification when only content changes.

  • AGENT.md discovery — Each package can include an AGENT.md — a structured guide written for AI consumption, not human prose. The registry indexes whether a package has one (has_agent_doc: true), and ailang pkg-docs vendor/name serves it directly.

  • Immutability — Once published, a version can never be overwritten. This means agents working concurrently can safely depend on a version without worrying about it changing underneath them.

  • Machine-readable metadata — ai_summary, tags, effects, and has_agent_doc are all in the registry index as structured JSON. Agents can programmatically search and filter without parsing documentation.

Typical Agent Workflow​

# 1. Discover what's available
ailang search "auth" --tag gcp

# 2. Read the AI-specific documentation
ailang pkg-docs sunholo/gcp-auth

# 3. Install with hash verification
# Package names use underscores - the registry name is canonical.
ailang install sunholo/gcp_auth@0.1.0

# 4. Use in code (a hyphen here parses as subtraction: PAR_HYPHEN_IN_IMPORT)
# import pkg/sunholo/gcp_auth/token (getAccessToken)

# 5. Publish reusable work back to the registry
ailang publish --dry-run # Validate first
ailang publish # Share with other agents

Writing Good AGENT.md​

Structure your AGENT.md for machine consumption:

  • Quick Start — minimal import + usage example (copy-pasteable)
  • Exported Functions — signatures with effect requirements
  • Common Patterns — typical workflows with the package
  • Effect Requirements — what --caps flags the consumer needs

Package Coordination Messages​

When packages are published or upgraded, the system emits structured coordination messages to notify affected workspaces. This enables multi-agent coordination at package scale.

Auto-Emitted Events​

Running ailang publish automatically emits:

  • upgrade-available — new version with change class (A=internal, B=additive, C=contract)
  • interface-change-notice — when exported API (interface hash) changed
  • effect-widening-warning — when effect ceiling expanded (e.g., added Net)

Package-Scoped Inboxes​

Messages route to typed inboxes: pkg:sunholo/auth, workspace:docparse, team:registry-admin.

# Notify downstream consumers
ailang pkg notify-upgrade sunholo/auth@0.2.0 --summary "Tightened validation"

# Find who depends on your package
ailang pkg affected-by sunholo/auth

# View coordination messages
ailang messages list --inbox pkg:sunholo/auth

For AI Agents​

When upgrading a dependency, check for coordination messages:

  1. ailang messages list --inbox workspace:<your-workspace> --unread
  2. Review any upgrade-available or interface-change-notice messages
  3. Run compatibility checks
  4. Send a compatibility-report back to the package inbox

See Agent Messaging Guide for full details.

Troubleshooting​

Common Errors​

ErrorCauseFix
409 Conflict on publishVersion already existsBump version in ailang.toml
401 UnauthorizedMissing or invalid API keySet AILANG_REGISTRY_API_KEY
hash mismatch on installCorrupted download or tampered packageRetry; if persistent, report to registry admin
effect ceiling violationPackage uses effects not in [effects].maxAdd missing effects to ailang.toml (note: Debug is exempt as a ghost effect)
package not foundWrong name or not yet publishedCheck spelling with ailang search
IMP010: symbol not exportedType missing export keywordAdd export type Foo = ...
LDR001: module not foundMissing dependency or wrong importAdd dep + ailang lock
cannot unify type constructor X with TRecordType alias not exportedAdd export type in defining module
PAR_HYPHEN_IN_MODULEHyphen in module pathUse underscores: billing_store

Common Pitfalls​

1. Hyphens in module names: Directory names can use hyphens (billing-store/) but module paths must use underscores (module sunholo/billing_store/...). Hyphens parse as subtraction.

2. Missing export type: Any type used by another package must have export type, not just type. This includes record types AND ADTs.

3. Use ./ for intra-package siblings, pkg/ for external: Three-way import distinction:

import ./plan (Plan, lookupPlan) -- LOCAL: sibling in same package
import pkg/sunholo/firestore/client (getDoc) -- EXTERNAL: different package
import std/result (Ok, Err) -- STDLIB: bundled

./ resolves in module namespace: if current module is a/b/c, then ./d means a/b/d.

Intra-package sibling imports — three equivalent forms. All three resolve to the same file and produce identical interface hashes. Pick whichever reads best:

-- 1. Bare canonical (matches what the imported module declares — useful when reading top-down)
import sunholo/linkedin/types (Profile)

-- 2. Relative (shortest; mirrors filesystem layout)
import ./types (Profile)

-- 3. Canonical with explicit pkg/ prefix (most explicit; matches consumer-side imports)
import pkg/sunholo/linkedin/types (Profile)

Forms 1 and 2 work even before ailang lock runs — useful while authoring a fresh multi-module package. External dependencies (any pkg/<other-vendor>/... import) still require ailang.lock. See examples/intra_package_imports/ for a runnable example.

4. Ok/Err not in prelude: Every file that uses Result needs import std/result (Ok, Err).

5. Run ailang lock after changing dependencies: The lockfile must be regenerated whenever [dependencies] changes.