Skip to content

Composition

Composition is ref resolution, not service launching.

A tactic can be called directly in process, through an HTTP service, or through a psi://... ref resolved from local config. LLLM keeps those choices at the boundary so the implementation can move without changing callers.

from lllm import TacticResolver

resolver = TacticResolver()
resolver.register("psi://demo/echo/tactics/echo", EchoTactic())

result = resolver.run("psi://demo/echo/tactics/echo", {"text": "hello"})

Local Service Binding

The same ref can point at a running service:

[refs."psi://demo/echo/tactics/echo"]
url = "http://127.0.0.1:8000/tactics/echo"

[refs."psi://demo/echo/tactics/echo".metadata]
policy_url = "http://127.0.0.1:9000"
resolver = TacticResolver.from_config(".")
tactic = resolver.resolve("psi://demo/echo/tactics/echo")

TacticResolver.from_config() reads the shared .psi/config.toml shape used across PSI packages. It loads tactic URL bindings and ignores refs owned by other layers.

Mixed Config Files

A single config file may include non-tactic refs from PsiHub or SSSN:

[refs."psi://demo/echo/tactics/echo"]
url = "http://127.0.0.1:8000/tactics/echo"

[refs."psi://demo/echo/services/api"]
url = "http://127.0.0.1:8000"

[refs."psi://demo/echo/channels/events"]
store = ".sssn"

LLLM uses the tactic binding. SSSN can use the channel binding. PsiHub can generate both. No layer has to launch another layer's services.

All binding keys must still be valid psi:// resource refs from known PSI resource sections. Shared config may include schemas, services, channels, snapshots, runs, configs, docs, examples, and assets, but malformed refs and unknown resource sections fail validation. Tactic ref segments are plain non-empty path segments; avoid whitespace, percent escapes, ., .., /, \, :, and ;. Config file paths and tactic URL bindings are read exactly; do not pad them with leading or trailing whitespace.

Targets And Metadata

A tactic ref with a concrete target must use url. Tactic URL bindings must be absolute HTTP(S) service URLs, and a tactic URL binding must not also declare a store, path, or object target. The store, path, or object target shapes belong to other layers or to direct in-process registration.

Service URL paths stay plain: avoid percent escapes, repeated slashes, dot segments, backslashes, and colons. URL bindings must not include URL params, query strings, fragments, embedded credentials, empty path segments, or dot segments in URL paths.

Use [refs."...".metadata] for structured binding metadata. Legacy top-level extra keys are also read as metadata, and the explicit metadata table wins on duplicate keys. Resolver-owned ref and remote url fields remain canonical.

Binding metadata must not include raw secret-shaped keys such as api_key/apiKey/apikey, tokens, accessToken/accesstoken, passwords, cookies, authorization, or credentials. Use local credential refs such as api_key_ref/apiKeyRef/apikeyref or auth hooks instead. Metadata maps must use string keys; direct Python metadata with non-string keys is rejected before Pydantic can coerce keys into text.

PsiHub validates and documents refs. It does not launch services.