Packages

Secure BEAM sandbox runtime for LLM code mode and MCP aggregation. Run concurrent LLM/tool clients safely while agents orchestrate approved tools, call upstream MCP/OpenAPI servers, and transform data.

Current section

Files

Jump to
ptc_runner docs guidelines development-guidelines.md
Raw

docs/guidelines/development-guidelines.md

# Development Guidelines
This is an Elixir library for Programmatic Tool Calling (PTC).
See **[Documentation](../../README.md)** for system design and API reference. See **[PTC-Lisp Specification](../ptc-lisp-specification.md)** for DSL details.
## Project Guidelines
- Run `mix format --check-formatted && mix compile --warnings-as-errors && mix test` before committing
- Use `Req` library for HTTP requests if needed, **avoid** `:httpoison`, `:tesla`, and `:httpc`
## Elixir Guidelines
- Elixir lists **do not support index-based access via the access syntax**
**Never do this (invalid)**:
i = 0
mylist = ["blue", "green"]
mylist[i]
Instead, **always** use `Enum.at`, pattern matching, or `List` for index-based list access:
i = 0
mylist = ["blue", "green"]
Enum.at(mylist, i)
- Elixir variables are immutable, but can be rebound, so for block expressions like `if`, `case`, `cond`, etc. you *must* bind the result of the expression to a variable if you want to use it:
# INVALID: we are rebinding inside the `if` and the result never gets assigned
if connected?(socket) do
result = compute()
end
# VALID: we rebind the result of the `if` to a new variable
result =
if connected?(socket) do
compute()
end
- **Never** nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors
- Don't use `String.to_atom/1` on user input (memory leak risk)
- Predicate function names should not start with `is_` and should end in a question mark. Names like `is_thing` should be reserved for guards
- Elixir's builtin OTP primitives like `DynamicSupervisor` and `Registry` require names in the child spec, such as `{DynamicSupervisor, name: MyApp.MyDynamicSup}`, then you can use `DynamicSupervisor.start_child(MyApp.MyDynamicSup, child_spec)`
- Use `Task.async_stream(collection, callback, options)` for concurrent enumeration with back-pressure. The majority of times you will want to pass `timeout: :infinity` as option
## Mix Guidelines
- Read the docs and options before using tasks (by using `mix help task_name`)
- To debug test failures, run tests in a specific file with `mix test test/my_test.exs` or run all previously failed tests with `mix test --failed`
- `mix deps.clean --all` is **almost never needed**. **Avoid** using it unless you have good reason
## Data Type Conventions
**Purpose**: Ensure consistency in internal data representation across the codebase. These conventions prevent bugs from type mismatches and unit confusion.
### Duration Storage
**Rule**: Store all durations as **integer milliseconds** in internal data structures. Convert to human-readable formats only at display time.
**Why milliseconds?**
- **Precision**: Captures sub-second durations accurately
- **No floating point errors**: Integers are exact
- **Industry standard**: Most systems use milliseconds internally
```elixir
# GOOD - Use integer milliseconds
%{duration_ms: 2500} # 2.5 seconds
# BAD - Don't use seconds or floats
%{duration_seconds: 2} # Too coarse
%{duration: 2.5} # Floating point errors
```
### Timestamp Storage
**Rule**: Always use `DateTime` with UTC timezone. Never use naive datetime.
```elixir
# GOOD
DateTime.utc_now()
# BAD
NaiveDateTime.utc_now() # Loses timezone context
```
## Library Design Principles
### DRY (Don't Repeat Yourself)
- Extract repeated logic into helper functions
- If you copy-paste code, refactor into a reusable function
- Exception: Duplication is better than the wrong abstraction
For test code, see [Testing Guidelines - Avoid Duplication](testing-guidelines.md#avoid-duplication).
### Public API
- Keep the public API minimal and well-documented
- Use `@moduledoc` and `@doc` for all public functions
- Consider using `@spec` for type documentation
- Hide implementation details in private functions
### Error Handling
- Use tagged tuples for expected errors: `{:ok, result}` or `{:error, reason}`
- Use exceptions for programmer errors (bugs)
- Provide meaningful error messages with context
### Process Safety
Since this library may run LLM-generated code:
- Only allow a fixed set of safe operations in the DSL
- Implement strict timeouts for program execution
- Consider memory limits for sandbox processes
- Log all tool calls for debugging and auditing
### Tool Boundary Conventions
Tools are called from PTC-Lisp with **named arguments only** (maps). Positional arguments are rejected with a clear error.
**Key type at boundary:** String keys (like JSON payloads)
- PTC-Lisp keywords (`:foo`) become string keys (`"foo"`) when passed to Elixir tools
- Prevents atom memory leaks from arbitrary LLM-generated keywords
- Matches how external data is typically handled in Elixir (Phoenix params, JSON APIs)
**Tool implementation pattern:**
```elixir
# Tools receive string-keyed maps
def run(%{"query" => query, "limit" => limit}, _ctx) do
# ...
end
# Or destructure in function body
def run(args, _ctx) do
query = args["query"]
limit = args["limit"] || 10
# ...
end
```
**Calling convention from PTC-Lisp:**
```clojure
(tool/search {:query "test" :limit 10}) ; map syntax
(tool/search :query "test" :limit 10) ; keyword-style (also valid)
(tool/search "test" 10) ; ERROR: positional args not allowed
```
### Testing
- Test the public API surface thoroughly
- Use property-based testing for parsers/interpreters where appropriate
- Include edge cases for malformed input
## Code Organization
See [Project Structure](../../AGENTS.md#project-structure) for the current layout.
```
lib/
├── ptc_runner.ex # Main public API: run/2, run!/2
├── ptc_runner/
│ ├── lisp/ # PTC-Lisp runtime
│ ├── sub_agent/ # SubAgent loop and logic
│ ├── sandbox.ex # Process isolation + resource limits
│ ├── context.ex # Variable bindings and tool results
│ ├── step.ex # Success/failure results
│ ├── plan_runner.ex # High-level mission execution
│ └── schema.ex # JSON schema utilities
```