@gencode/uni
v0.8.6
Published
Independent unified agent runtime for AIMax.
Readme
@gencode/uni
Independent unified agent runtime for AIMax.
@gencode/uni does not reuse the existing @gencode/cli or @gencode/agents
run pipeline. It provides:
runUni()for one-shot execution- pluggable runtime agents (
claude-code,codex,opencode,pi,acp) - callback and websocket transports
- a normalization layer that maps provider-native output to
@gencode/sharedrun events - per-run
record_artifactstool bridges for Claude Code, Codex, OpenCode and Pi; ACP intentionally remains unsupported
Session artifacts.json V2 stores file-operation audit in operations and
cumulative explicit final artifacts in artifacts. Neither the final run
result nor transcript entries carry an artifacts field. Providers call
record_artifacts promptly after successfully producing a requested Markdown
or HTML report/document, or a 神兵文档, Wizard Wiki, or Pages URL. The call is
production-triggered rather than a finalization checkpoint, and runs without an
eligible deliverable do not make an empty declaration. Uni exposes the provider
tool bridge without appending artifact instructions to the caller's text or
Codex structured messages, so provider input stays exactly as supplied by the
caller.
This package is intentionally standalone. Existing CLI integration remains a separate concern.
CLI
@gencode/uni also exposes a standalone binary:
gencode-uni run \
--runtime-agent codex \
--data-dir /data/user1 \
--message "Review this repository" \
--output jsonl \
--runtime-cli-path "codex --proxy" \
--runtime-mode full-auto \
--runtime-provider-base-url https://relay.example.com/v1The command keeps the current aimax run parameter surface for core fields and
adds runtime-specific flags inspired by reference/cc-connect, such as:
--runtime-cli-path--runtime-cli-args-flag--runtime-env KEY=VALUE--runtime-mode— runtime/approval mode. When omitted forcodex, defaults toyolo(no sandbox, auto-approved commands, network access). Passsuggest,auto-edit, orfull-autoto opt into a stricter sandbox.--runtime-reasoning-effort--runtime-router-url--runtime-router-api-key--runtime-codex-home--runtime-provider-env--runtime-provider-header
For API callers, top-level runtimeConfig.env is applied to every spawned
provider process. The CLI's repeatable --runtime-env KEY=VALUE option is
applied to the selected provider environment. Effective environment precedence
is Uni's provider-state fallback, inherited process values, top-level
runtimeConfig.env, --runtime-provider-env (where supported), and finally the
selected provider's environment (--runtime-env); dedicated options such as
--runtime-codex-home override the corresponding environment key. The same
effective provider-state path is used both to launch the child and to verify
legacy Resume evidence.
Codex API callers may provide inline structured messages instead of
message or fromFile. The value must be one Message object or a non-empty
Message array using the same structured Message contract as file input, and
exactly one of the three input sources may be present. This compatibility is
Codex-specific; other Uni providers continue to use message or fromFile.
For a Codex run, the effective CodeProxy API key resolves in this order:
apiKey, AIMAX_API_KEY, then AIMAX_LLM_API_KEY. Codex does not derive an
API key from authToken; the existing auth-token conversion behavior remains
unchanged for other Uni providers.
Skill load paths
Codex, Pi and OpenCode accept additional Skill roots through the comma-separated
--skillsLoadPaths option (or UniRunOptions.skillsLoadPaths):
gencode-uni run \
--runtime-agent pi \
--data-dir /data/user1 \
--skillsLoadPaths /mnt/shared-a,/mnt/shared-b \
--message "Use the available skills"Each caller-supplied entry must be an absolute path. Uni trims entries, drops
empty values, and removes exact duplicates while keeping their first position.
Caller copies of /aimax/skills or <dataDir>/.aimax/skills are filtered out
before Uni restores those implicit roots at their fixed first and last
positions.
For every real Codex, Pi or OpenCode run, including Resume, Uni resolves these roots in
low-to-high logical priority order:
/aimax/skills
→ --skillsLoadPaths entries in caller order
→ <dataDir>/.aimax/skillsUni does not create these source directories or parse their SKILL.md
contents. Missing source roots are skipped. Provider-native discovery remains
active.
- OpenCode receives the roots in
OPENCODE_CONFIG_CONTENT.skills.pathsin the order above. Existing JSON/JSONC configuration is preserved; caller-ownedskills.pathsentries are retained after the Uni-managed roots, and exact duplicate paths are emitted once. - Pi receives repeated
--skill <path>arguments in reverse order because Pi keeps the first discovered same-named Skill: user root, reversed caller roots, then/aimax/skills. - Codex scans each root's immediate children and symlinks directories containing
SKILL.mdinto<effective-CODEX_HOME>/skills. Later roots win duplicate names. Uni records its links in<effective-CODEX_HOME>/.aimax-managed-skills.json, removes stale managed links on later runs, and never overwrites caller-owned Codex entries. A link collision or an externally changed managed link fails before Codex starts. - Claude Code and ACP continue to reject non-empty values explicitly and do not receive the automatic system or user roots.
Smoke Tests
Provider-specific helper scripts live in scripts/:
scripts/test-claude-run.shscripts/test-codex-run.shscripts/test-opencode-run.shscripts/test-pi-run.shscripts/test-one-shot-resume.sh
To run the OpenCode provider through @gencode/uni:
cd source/packages/uni
./scripts/run-opencode-with-uni.sh "Summarize this repository"The Pi provider uses the official pi command:
cd source/packages/uni
./scripts/test-pi-run.sh "Summarize this repository"The helper defaults to UNI_OUTPUT=jsonl, so callback-compatible events are
written to stdout one JSON object per line as the provider emits them. Use
UNI_OUTPUT=json when you need the final aggregated result object instead.
Useful overrides:
OPENCODE_BIN=/path/to/opencode \
UNI_DATA_DIR=/tmp/aimax-uni \
UNI_PROJECT_DIR=/path/to/repo \
UNI_RUNTIME_PROVIDER_API_KEY="$OPENAI_API_KEY" \
UNI_RUNTIME_PROVIDER_BASE_URL="https://api.example.com/v1" \
UNI_RUNTIME_PROVIDER_MODEL="gpt-4.1" \
./scripts/run-opencode-with-uni.sh "Review the current project"UNI_SESSION_ID is always the public logical Session ID. It names the
directory under <dataDir>/.aimax/sessions/ and remains unchanged in stdout,
callback, websocket, and final result events. Do not pass a provider-native
session/thread ID here; Uni stores that private Resume binding only in
session.json.runtimeBinding.providerSessionId.
To exercise a real two-turn Resume in separate one-shot processes, run:
cd source/packages/uni
UNI_DATA_DIR=/tmp/aimax-uni-resume \
./scripts/test-one-shot-resume.sh opencodeReplace opencode with claude-code, codex, or pi as needed. The smoke
script makes two real provider calls, so credentials, network access, and any
provider charges apply. It verifies that both public results use the same
logical ID, the provider binding remains stable, all four base Session files
exist, the second run recalls the first-turn marker, and the transcript grows.
For a real replacement-container test, split the phases across containers that share the same persistent data mount:
# first container
UNI_RESUME_PHASE=first \
UNI_SESSION_ID=resume-smoke-1 \
UNI_DATA_DIR=/data/user1 \
./scripts/test-one-shot-resume.sh pi
# replacement container, with /data/user1 mounted from the same volume
UNI_RESUME_PHASE=second \
UNI_SESSION_ID=resume-smoke-1 \
UNI_DATA_DIR=/data/user1 \
./scripts/test-one-shot-resume.sh piThe first phase writes a non-sensitive validation checkpoint under
<dataDir>/.aimax/resume-smoke/ and does not copy a provider-native ID into
that checkpoint. Within AIMax-owned Session metadata, the native ID remains
only in session.json.runtimeBinding.
By default Uni keeps provider-native state below the same persistent data root:
| Runtime | Default native-state path | Controlling environment |
| --- | --- | --- |
| Claude Code | <dataDir>/.aimax/provider-state/claude-code | CLAUDE_CONFIG_DIR |
| Codex | <dataDir>/.aimax/provider-state/codex | CODEX_HOME |
| OpenCode | <dataDir>/.aimax/provider-state/opencode | XDG_DATA_HOME points at <dataDir>/.aimax/provider-state |
| Pi | <dataDir>/.aimax/provider-state/pi | PI_CODING_AGENT_DIR; sessions use PI_CODING_AGENT_SESSION_DIR |
An explicitly supplied provider-state environment variable takes precedence.
When running in containers, that override must also point at a persistent mount;
persisting only session.json is insufficient for provider-native Resume.
Codex CodeProxy default
For a Codex run, Uni creates <effective-CODEX_HOME>/config.toml when that file
does not exist. Without a CODEX_HOME override, the path is
<dataDir>/.aimax/provider-state/codex/config.toml. The generated configuration
selects the aimax-codeproxy model provider at
http://127.0.0.1:8787/v1 with wire_api = "responses"; it contains no API
key or Authorization header. An existing config.toml remains byte-for-byte
unchanged.
For every run, Uni creates or overwrites
<effective-CODEX_HOME>/custom-models.json with one metadata entry whose slug
is the run's effective model. The managed entry declares a 128000 token
context window and conservative text-only capabilities. Codex receives an
explicit model_catalog_json CLI override pointing to that absolute path, so
the refreshed catalog is also used when a caller-owned config.toml already
exists.
Before every new run or Resume, Uni also reconciles the system, caller-provided,
and user Skill roots into <effective-CODEX_HOME>/skills as managed symlinks.
This keeps Codex-native Skill discovery available even though Codex has no
separate Skill-root CLI option. Existing non-managed entries are preserved and
reported as explicit collisions instead of being overwritten.
Before spawning Codex, Uni probes http://127.0.0.1:8787. A reachable
CodeProxy is reused and never closed by Uni. Otherwise Uni starts an in-process
CodeProxy on port 8787 and closes only that owned instance after the run.
The embedded proxy translates Codex Responses requests to an OpenAI-compatible
Chat Completions upstream using the run's effective baseUrl, credentials and
model. Before starting the embedded proxy, Uni removes one or more trailing
slashes from the effective baseUrl, so values such as https://host/v1/ are
used as https://host/v1. AIMax logical Session, message, channel and model
headers are forwarded to that upstream. Callers supplying an external
CodeProxy remain responsible for its upstream configuration and lifecycle.
Legacy Uni Sessions are migrated only when their transcript consistently names
one runtime and the matching provider-state tree contains that provider's known
authoritative session lifecycle record. For Claude Code this means
system + subtype:init; ordinary assistant/result frames, a matching filename,
or a generic nested id are not accepted as proof. When the mapping cannot be
established, Resume fails before starting the provider instead of silently
creating a new conversation.
Uni uses only the logical Session ID on public event fields. Once a
provider-native ID is known, exact occurrences are redacted from all public and
non-binding Session payloads, including assistant text, thinking, tool payloads,
titles, model labels, errors, diagnostics, stdout, callback, websocket, and
transcript fields. The native ID remains in
session.json.runtimeBinding.providerSessionId, provider-owned state, and the
provider Resume call.
Provider tool normalization is evidence-based. Codex command and MCP calls are
paired only from observed start/completion frames, and MCP names use
mcp:<server>:<tool> so remote tools are not mistaken for local file tools. A
completed-only Codex file_change is audited internally as a terminal file
operation without a synthetic public tool start or transcript tool pair.
OpenCode completed-only write and edit terminal events are likewise audited
when they are successful and contain complete structured local-file input; they
do not create synthetic transcript tool pairs. Pi
toolcall_delta frames are not assistant text; only Pi text_delta and
thinking_delta update those corresponding streams. Because all four aligned
adapters request native JSON event modes, non-JSON stdout remains raw and is not
invented as assistant text.
