Functions in other languages
In AEL, the Agent Engineering Language, your application can hold private functions written in other languages, with typed AEL signatures, built and run with your own toolchains, pinned by version. You write the body in its own language and the signature in AEL, so the rest of your program calls the function like any other, and the check holds every call to its types. This page describes functions in other languages in AEL 1.0.0.
Two ways to write one
| Form | Where it is written | What it declares |
|---|---|---|
| A language file | <name>.<lang>.ael, such as profile.python.ael | One function in that language, <lang> <name>(…), as the file's primary. |
| An inline function | main.ael, the file of any component except a hook, a configuration or a skill, and their extension files | fn <lang> <name>(…), beside your AEL functions. |
<lang> is the tag of a language from the list of languages, in lower case, such as python, bash or golang.
A language file
# src/profile.python.ael
python profile(x: i64) -> i64 [version = "3.12", mode = machine] {
return x * 2
}
- The primary starts with the language's tag, then the file's name:
profile.python.aeldeclarespython profile(…), and the check refuses another language or another name. - The signature,
(x: i64) -> i64, is AEL. The body, between the braces, is Python. - A language file can also hold private helper functions, in AEL or in its own language.
Its extension files hold more functions of the same language, in the same folder:
src/
profile.python.ael
profile.python.extended.ael
profile.python.extended.2.ael
# src/profile.python.extended.ael
fn python clamp(x: i64, limit: i64) -> i64 {
return min(x, limit)
}
- An extension file of a language file holds only
fn <lang>functions of that file's language: no AEL function, no other language and no header. - Its functions belong to the language file, as the helpers of any extension file belong to its component; see Extension files.
An inline function
# src/nodes/score.node.ael
node score(points: i64) -> i64 {
return scale(points, 3);
}
fn python scale(p: i64, k: i64) -> i64 {
return p * k
}
fncomes first, then the language's tag, the name and the AEL signature.- An inline function sits at the top level of its file, beside your AEL functions, never inside another function.
- A component's extension files can hold inline functions in any language of the list, beside its AEL helpers. The extension files keep their names:
score.node.extended.aelholdsfn pythonfunctions without a language in its name.
Names that are refused
- A language and a role in one name, in either order:
profile.python.reuse.aelandprofile.reuse.python.extended.aelare refused. A component's file and its extension files never carry a language; they holdfn <lang>functions instead. - A language that is not in the list, or a tag in another letter case, such as
profile.Python.ael. - A role that collides with a language. No language takes the name of a role, such as
routes, and a role that a package adds can never take the name of a language, so the check never has to guess which one a file means. - An extension numbered 1 or with a leading zero, such as
profile.python.extended.1.ael, as for every extension file.
Private, always
- Every function in another language is private to the file that declares it and that file's extension files.
pub fn pythonis refused, like everypub fn. No import, reference or function value makes such a function public.- The primary of a language file is private to that file and its extension files too.
Other files reach your code in another language through a function written in AEL: a reusable or global function that wraps a private helper in that language.
# src/scoring.reuse.ael
reuse scoring(points: i64) -> i64 {
return weigh(points);
}
fn python weigh(points: i64) -> i64 {
return points * 3 + 1
}
# src/nodes/rank.node.ael
import "../scoring.reuse.ael";
node rank(points: i64) -> i64 {
return scoring(points);
}
rank calls scoring through its import and never sees weigh. Functions and visibility gives the sharing rules that every function follows.
Options
One optional record in square brackets follows the result type, before the {, as in [version = "3.12", mode = machine]. It holds at most these three fields, each one once, separated by commas:
| Option | Value | What it does |
|---|---|---|
version | An exact version in quotes: one to four numbers separated by dots, such as "3.12" or "21" | The toolchain must report this version, compared at the precision you write: "3.12" takes a 3.12 release and never 3.13. Another version is refused, never substituted, and AEL never downloads a runtime. Without version, the version that is installed is used, and its exact identity is recorded. |
mode | machine, the default, or sandbox | Where the function runs: on the same machine as your program, with the toolchain you installed, or in a sandbox. |
shell | The file name of a shell program, in quotes | For bash and shell functions only: the shell that runs the body. A name only, with no path, arguments or flags. |
- Ranges and wildcards, such as
"3.x"or">=3", are refused, and so are an unknown field, a field written twice and a trailing comma. - A function in another language runs on the same machine by default or in a sandbox, which needs the isolation facilities of Linux and runs only on the Linux target the documentation names for it, and a missing toolchain, a wrong version or a missing sandbox is refused before anything runs.
- A function in sandbox mode never runs on the same machine instead. Supported targets names the target where the sandbox runs.
- Machine mode does not mean unlimited permissions: what a function may use comes from your deployment, as Running describes.
Sandbox mode and isolated components
Isolated components run in a sandbox too. You mark an agent, node or subagent as isolated: it always runs in a sandbox in its own process, outside the shared communication layer, reachable only through the edges you declare and holding only the resources it declares itself. Isolation needs the isolation facilities of Linux and runs only on the Linux target the documentation names for it: on macOS, on Windows, on microcontroller boards, on other Linux targets and on any host without those facilities, an isolated component is refused, never run without isolation.
Inside an isolated component, a function in another language runs only in sandbox mode: one in machine mode, the default, is refused there.
The body
- The line of the declaration ends with
{, and the body starts on the next line. - The body ends at the first line that starts with
}in its first column, with nothing after it but spaces or tabs. - Everything in between belongs to the other language, byte for byte: AEL reads no comment, string or escape inside it, and changes no indentation. An indented
}is part of the body. - A body cannot hold a line that is only a
}in its first column: indent that line, or write the code another way. - A body on the declaration's own line, and a file that ends before the closing line, are refused.
Every source file stays small and reviewable: a fixed size limit per file, with private helpers split into extension files. Bodies count toward the limits of their file, at most 5,000 bytes and at most 5,000 lines, with every byte of every body included. A body fits in one file and never continues in another, so when code grows, you split it into several functions in the file's extension files. Limits gives the exact rules.
Types a signature can use
The parameters and the result of a function in another language are typed in AEL, and every value crosses by value, in both directions:
| AEL type | How it crosses |
|---|---|
() | As a parameter or the result. |
bool | As a parameter or the result. |
i8, i16, i32, i64, u8, u16, u32, u64 | At its exact width and sign; every value keeps its precision, the largest and the smallest included. |
Str<N> | As valid UTF-8 text of at most N bytes. |
Vec<T, N> | As a list of at most N elements, whose T is one of the types above. |
Result<T, ForeignError> | As the result only, with T one of the types above. |
- Nothing else crosses: no reference, struct, enum,
Option, nestedResult, list of lists or function. The check refuses such a signature at its declaration, before any toolchain starts. - A function has at most 128 parameters, and a capacity
Nis at most 16,777,216. - A value that does not fit its type, such as text longer than
Nbytes, is a failure; it is never cut short. - A
binaryorassemblyfunction uses only(),booland the fixed-width integers.
ForeignError is AEL's own error type for these calls, and the only error type such a signature takes. It holds the status, up to 4,096 bytes of the function's standard error, and its kind:
| Kind | What happened |
|---|---|
Launch | The function could not start. |
Exit | It ended with a failure status. |
Signal | The operating system stopped it. |
Timeout | It passed its deadline. |
Cancelled | It was cancelled. |
Decode | Its result did not decode into the declared type. |
Budget | It reached a limit of its budget. |
Unavailable | Its toolchain or runtime was not available. |
With a plain result type, a failure is never turned into a value of that type. Declare Result<T, ForeignError> to handle failures in your code.
Building
When you build, AEL checks every function in another language first, then compiles each one with its own toolchain:
- Checked before any toolchain starts. File names, privacy, options, signatures, the limits of the bodies and what the target can offer are checked first.
- Your toolchain, at its pinned version. Each function is compiled by the toolchain you installed for its language, at the version it pins. A missing toolchain or another version refuses the build, and nothing is written.
- Compiled, never run. Building checks and compiles your code in the other language; it never runs your function. A Python function's source is checked and compiled to bytecode, a bash or shell function is checked for its syntax, and the other languages are checked or compiled by their own toolchains.
- Every function. Every function you declare is compiled, including one that nothing calls; only what your program reaches goes into it.
- Beside your program. The compiled code of your functions is written beside the program, and a failed build leaves no part of it behind.
- Nothing downloaded. The build never downloads or installs a toolchain or a runtime.
- Nothing without a declaration. A project that declares no function in another language starts no toolchain at all.
Building a program describes the build.
Running
- The runtime you installed. Where your program runs, each function runs with the runtime its language needs, at the version it pins, in the mode it declares. A missing runtime, another version or a missing sandbox fails the call before the function starts.
- Arguments stay data. Arguments are passed as typed values, never spliced into a command line or into the text of a script.
- Results are checked. The result is decoded into its AEL type, and a result that does not decode is a
Decodefailure, never a value. A function's exit status is never read as its result. - Limits, deadlines and cancellation. The working directory, the environment values, the files, network and processes a function may use, and its time, memory and output limits come from your deployment, through the call that runs it, and the function draws on the budget of the run that calls it. A cancellation or a passed deadline stops the function and every process it started.
- Unknown stays unknown. After a cancellation, an effect outside your program whose outcome is not known is reported as unknown, never erased.
- No secrets in source. Credentials never appear in your source or in compiled files.
- Only when you reach one. A runtime enters your program only when the program declares a function in another language that it reaches. A program without one carries nothing for them and needs no runtime.
Machine code: binary and assembly
binary and assembly need no toolchain of yours: AEL itself checks them, for the one target the function is written for.
- A
binaryfunction's body is machine code, and AEL decodes and verifies every byte of it. - An
assemblyfunction's body is instructions, and AEL assembles and verifies each one. - AEL refuses code that it cannot verify, and code for another target.
- These functions take no
version.
What stays in AEL
main.main.aeland its extension files can holdfn <lang>functions thatmaincalls, butmainitself is written in AEL.- Hooks. A hook file and its extension files hold no function in another language; see Hooks.
- Configuration and the control files. Configuration files,
pack.aelandmetadata.aelare AEL. - Skills. A skill holds AEL only; see Skills.
- Packages. A package project that holds a language file or a function in another language is refused: functions in other languages belong to applications. See Packages.
A function, or a program you already have
A function in another language is code you write in your application, compiled with it when you build. To call a program that you already have, without changing it, your agent uses the execution packages instead. Calling your existing programs compares the two.
In AEL Cloud
In AEL Cloud, functions in other languages may be written in bash, shell or Python and always run in a sandbox, and a program that declares any other language is refused when you submit it. AEL Cloud describes the service.
Distributions
AEL 1.0.0 is the full distribution; smaller distributions of the same version leave out components, such as support for other languages or for agents, and refuse a feature they leave out by naming it. A distribution without support for other languages refuses every function in another language; Distributions describes them.
The languages
These are the languages you can write a function in, with the tag that names each one in a file name and after fn:
| Language | Tag | Kind |
|---|---|---|
| Ada | ada | Compiled to native code |
| Assembly | assembly | Verified by AEL |
| BASIC | basic | Compiled to native code |
| Bash | bash | Interpreted |
| Binary | binary | Verified by AEL |
| Bun | bun | Interpreted |
| C | c | Compiled to native code |
| C# | csharp | Compiled for a managed runtime |
| C++ | cpp | Compiled to native code |
| Clojure | clojure | Compiled for a managed runtime |
| COBOL | cobol | Compiled to native code |
| CSS | css | Recognized, not callable |
| Dart | dart | Compiled for a managed runtime |
| Delphi | delphi | Compiled to native code |
| Elixir | elixir | Compiled for a managed runtime |
| Erlang | erlang | Compiled for a managed runtime |
| F# | fsharp | Compiled for a managed runtime |
| Fortran | fortran | Compiled to native code |
| Go / GoLang | golang | Compiled to native code |
| Groovy | groovy | Compiled for a managed runtime |
| Haskell | haskell | Compiled to native code |
| HTML | html | Recognized, not callable |
| Java | java | Compiled for a managed runtime |
| JavaScript | javascript | Interpreted |
| Julia | julia | Interpreted |
| Kotlin | kotlin | Compiled for a managed runtime |
| Lisp | lisp | Interpreted |
| Lua | lua | Interpreted |
| MATLAB | matlab | Interpreted |
| .NET | dotnet | Compiled for a managed runtime |
| Objective-C | objectivec | Compiled to native code |
| Pascal | pascal | Compiled to native code |
| Perl | perl | Interpreted |
| PHP | php | Interpreted |
| Prolog | prolog | Interpreted |
| Python | python | Interpreted |
| R | r | Interpreted |
| Ruby | ruby | Interpreted |
| Rust | rust | Compiled to native code |
| Scala | scala | Compiled for a managed runtime |
| Scheme | scheme | Interpreted |
| Scratch | scratch | Recognized, not callable |
| Shell | shell | Interpreted |
| SQL | sql | Recognized, not callable |
| Swift | swift | Compiled to native code |
| TypeScript | typescript | Compiled for a managed runtime |
| Verilog | verilog | Recognized, not callable |
| VHDL | vhdl | Recognized, not callable |
- HTML, CSS, SQL, VHDL, Verilog and Scratch are recognized but not callable: the check keeps their bodies unchanged, and refuses a call to one of them and a build that would run one.
- AEL knows exactly these tags. It never guesses a language from a body, and never downloads a tool to add one.
Honest limits
- AEL checks the signature, the options and the limits of a function in another language, not the code inside its body: what that code does is up to its own language and toolchain.
- Sandbox mode needs the isolation facilities of Linux and runs only on the Linux target the documentation names for it. Wherever it does not run, a function that declares it is refused, never run on the same machine instead.
- Each toolchain and runtime is yours to install, at the version your code pins, on the machine that builds the program and on the machine that runs it.