Metadata-Version: 2.4
Name: gl-skill-binary
Version: 0.1.4
Summary: Standalone GL Skill loader and bounded asynchronous executor.
Author: Gen AI SDK Team
License: Apache-2.0
Requires-Python: <3.14,>=3.11
Description-Content-Type: text/markdown
Requires-Dist: gllm-core-binary<0.5.0,>=0.4.58
Requires-Dist: gllm-inference-binary[openai]<0.7.0,>=0.6.130
Requires-Dist: jsonschema>=4.26.0
Requires-Dist: aiohttp>=3.14.4
Requires-Dist: certifi>=2026.10.19
Requires-Dist: langgraph>=1.2.14
Requires-Dist: langgraph-sdk>=0.4.6
Requires-Dist: multidict>=6.9.1
Requires-Dist: oauthlib>=4.0.0
Requires-Dist: opentelemetry-api>=1.45.1
Requires-Dist: pyasn1>=0.6.5
Requires-Dist: pyjwt>=2.15.1
Requires-Dist: pyyaml<7.0,>=6.0
Requires-Dist: urllib3>=2.8.0

# GL Skill

`gl-skill` is a standalone Python library for loading one local Skill and running one bounded
model/tool loop. Clients start a run through `run()` and consume ordered events plus
one terminal result; they do not resume unfinished tool turns.

An application (for example, AIP) initiates every run. GL Skill composes its **Loader** and
**Executor**, while **Tool Runtime** is the dispatch authority for admitted capabilities. Built-in
workspace operations flow through `WorkspaceToolRuntime` and `SandboxedWorkspaceRuntime` into
[gl-sandbox](../gl-sandbox); caller tools are separately registered host implementations.
GL Skill never asks AIP to continue an
unfinished tool turn.

## Quick start

After installing `gl-skill` and setting `OPENAI_API_KEY`, an instruction-only local Skill needs
only its directory and a query. The default capability allowlist is empty, so this path does not
create a workspace:

```python
import asyncio

from gl_skill import GLSkill, SkillRunResult


async def main() -> None:
    result = None
    async for item in GLSkill.run(skill="./skills/hello", query="Say hello."):
        if isinstance(item, SkillRunResult):
            if not item.succeeded:
                detail = item.error.message if item.error is not None else f"status={item.status.value}"
                raise RuntimeError(f"GL Skill run failed: {detail}")
            result = item
    assert result is not None
    print(result.output_text)


if __name__ == "__main__":
    asyncio.run(main())
```

This normal-file example requires an existing `./skills/hello/SKILL.md`. From `libs/gl-skill`, run
the live default-model path after explicitly supplying its process-level credential prerequisite:

```bash
# Set OPENAI_API_KEY in this process through your credential manager first.
uv run python examples/hello_world.py
```

This command makes a paid provider request through the configured default model route. GL Skill
does not read `.env`; the key must already be in the process environment. The deterministic
convergence test exercises this same facade and asserts the printed greeting without making a
network request.

`GLSkill.run()` streams the same ordered lifecycle. Advanced callers can import `GLSkillClient`,
`PublicRunRequest`, and `PublicDependencies` to inject a provider-neutral model runtime, caller
capabilities, event sink, or sandbox backend explicitly. The facade never searches for `.env` files;
the default model path reads only the process-level `OPENAI_API_KEY` value.

For migration details and the typed stream contract, see
[`docs/migration-run-only.md`](docs/migration-run-only.md).

The Loader and Executor are packaged together in `gl-skill`. Architecture contracts live in
[contracts/](contracts/README.md), with design material under [docs/architecture/](docs/architecture/README.md).

## Loading a Skill for a host agent

Contract 2.0 is the recommended opt-in entry point for new host integrations. The v1
`SkillProvider.load()` contract stays the default and remains supported unchanged. Call
`provider.load_result(...)`, available on `LocalSkillProvider` and its subclasses
(`load_skill_result(provider, ...)` is the equivalent free function for any `SkillProvider`). It
returns an immutable `SkillLoadResult` containing deterministic resource summaries, omitted
symlinks, a shell-usable `skill_root`, and structured error candidates:

```python
import asyncio
from pathlib import Path
from gl_skill import LocalSkillProvider, SkillLocatorData


async def main() -> None:
    provider = LocalSkillProvider()
    locator = SkillLocatorData(
        source=Path("skills").resolve().as_uri(),
        ref="hello",
    )
    result = await provider.load_result(locator, workspace_root=Path.cwd())

    if result.status == "ok":
        print(f"Loaded {result.name} from {result.skill_root}")
        for resource in result.resources:
            print(f"  {resource.path}: {resource.summary}")

        # Host staging: reuse result.document with WorkspaceToolRuntime
        document = result.document
        assert document is not None
        # Same result, pointing at the staged copy of the Skill directory:
        staged_result = result.with_skill_root("/staged/workspace/skill")
        # Send only this compact text to the model (never to_wire()). It wraps the instructions
        # in <skill_content>, lists at most gl_skill.loader.DEFAULT_MAX_RESOURCES resources by default,
        # and omits hashes, sizes, provenance and host diagnostics:
        print(staged_result.to_model_text())
    else:
        assert result.error is not None
        print(f"Load failed: {result.error.code} - {result.error.message}")
        if result.available_skills:
            print("Available retry candidates:")
            for candidate in result.available_skills:
                print(f"  {candidate.name}: {candidate.description}")
        # The model gets <skill_error> with the constant message and the retry candidates:
        print(result.to_model_text())


asyncio.run(main())
```

## Install

GL Skill supports Python 3.11–3.13. Source builds declare
`gllm-inference[openai]>=0.6.130,<0.7.0` as a required runtime dependency, alongside `gllm-core`,
`jsonschema`, and `pyyaml`. Importing `gl_skill` requires no model credentials or sandbox backend;
the default model path requires `OPENAI_API_KEY` when a run starts.

With an activated virtual environment, build and install the source wheel from `libs/gl-skill`:

```bash
uv build
uv pip install dist/gl_skill-*.whl
python -c "import gl_skill"
```

The executor's default workspace policy is schema-complete: 100 files, 10,000,000 total bytes,
1,000,000 bytes per file, 20-second command timeout, 100,000 command-output bytes, no environment
variables, and network access disabled. Unknown or malformed policy fields fail closed; network
capabilities are not admitted, and sandbox-required capabilities must be validated
built-in workspace capabilities.

The repository's binary build pipeline produces the separate `gl-skill-binary` distribution and
rewrites SDK dependencies to their corresponding `-binary` distributions. To install a binary
artifact, use its wheel and let its metadata select the required dependencies:

```bash
uv pip install dist/gl_skill_binary-*.whl
```

Both distributions expose `gl_skill`; the GLLM runtime exposes `gllm_inference`. Choose one
distribution and, for binary artifacts, a wheel compatible with your Python version and platform.
GLLM imports remain lazy when callers inject a custom `ModelRuntimeProtocol`, but the declared
runtime dependencies are still installed.

Dependency resolution may require access to the configured internal indexes. Supply any required
`UV_INDEX_GEN_AI_INTERNAL_*` / `UV_INDEX_GEN_AI_*` credentials through the process environment.

## Inputs and configuration

GL Skill reads no `.env` file implicitly and performs no environment discovery beyond the documented
default-model path. Compose these inputs explicitly:

- **Skill source:** an absolute local `file:///<skills-root>` URI plus a containment-safe single directory-component
  `skill_ref`. The referenced directory must contain a regular `SKILL.md`.
- **Request:** caller identity, correlation ID, query, stable allowed capability IDs, model ID, run
  limits, and workspace policy.
- **Dependencies:** a Skill provider, an optional custom model runtime, an optional `SandboxBackend`,
  registered caller tools, and an optional event sink. If no model is injected, `OPENAI_API_KEY` must
  already be present in the process environment and the installed GLLM runtime supplies the default.
- **Workspace:** an explicit backend object is required before built-in `workspace.*` capabilities
  can be admitted. There is no automatic sandbox selection or credential discovery.

## Advanced explicit composition

This first example creates one temporary local Skill and runs it with a scripted model. It has no
network access and requires only the core development environment:

```bash
cd libs/gl-skill
make setup
uv run python examples/quickstart_run.py
```

Expected terminal summary:

```text
status=succeeded output='The note was saved.'
```

The complete deterministic source is
[`examples/quickstart_run.py`](examples/quickstart_run.py). The installed-wheel
convergence gate executes that canonical file on Linux and Windows. Replace its
scripted model with a real model adapter when your application owns that
dependency.

A successful result contains one authoritative terminal status, final text, typed receipts, evidence,
and usage. Default output follows the
[run 2.0 result schema](gl_skill/contracts/schemas/skill-run-result.v2.schema.json).
Explicit `output_version="1.0"` follows the
[legacy result schema](gl_skill/contracts/schemas/skill-run-result.schema.json).
Examples for both versions are in [contracts/examples/](contracts/examples/).

## Streaming

Use `client.run(request)` when the initiating application wants ordered progress. When consumed to
completion, it yields events and then exactly one `SkillRunResult`:

```python
from gl_skill import SkillRunResult

async for item in client.run(request):
    if isinstance(item, SkillRunResult):
        print(f"result={item.status.value}")
    else:
        print(f"event={item.type} sequence={item.sequence} terminal={item.terminal}")
```

Closing the stream early ends observation and runs bounded cleanup, but does not deliver a normalized
cancelled terminal item to the detached consumer. Use `contextlib.aclosing()` (or explicitly await
`stream.aclose()`) when breaking early so cleanup is prompt:

```python
from contextlib import aclosing

from gl_skill import SkillRunResult

async with aclosing(client.run(request)) as stream:
    async for item in stream:
        if should_stop_observing(item):
            break
        if isinstance(item, SkillRunResult):
            print(item.status.value)
```

Cancelling the consumer task instead runs cleanup and propagates `asyncio.CancelledError`, with no
terminal-delivery guarantee. To receive a cancelled terminal event and `SkillRunResult`, pass an
`asyncio.Event` as `cancellation_token` to `GLSkillClient.run()` and remain attached through the final
item.

The runnable version also uses the same temporary Skill and scripted model:

```bash
uv run python examples/streaming_run.py
```

## Tools, resources, policy, and events

### Stable capability IDs and model-visible names

Admission uses stable versioned IDs such as `workspace.read@1` and `caller.echo@1`. Model-visible
names such as `workspace_read` and `caller_echo` exist only in schemas projected to the model. Do not
persist model-visible names as authorization identities.

### Caller tools

Caller handlers are trusted host callbacks registered by the application. They execute in the host
process and do **not** inherit the gl-sandbox guarantee. Validate inputs at their boundary and give
them truthful effects metadata:

```bash
uv run python examples/caller_tool.py
```

See [examples/caller_tool.py](examples/caller_tool.py) for complete input/output JSON Schemas,
effects, handler registration, receipt inspection, and expected offline output.

### Sandboxed workspace operations

Four built-in capabilities are available when an explicit backend is supplied:

| Stable capability ID | Model-visible name | Operation |
| --- | --- | --- |
| `workspace.list@1` | `workspace_list` | List staged files |
| `workspace.read@1` | `workspace_read` | Read bounded file content |
| `workspace.write@1` | `workspace_write` | Write bounded bytes |
| `workspace.execute_command@1` | `workspace_execute_command` | Execute exact argv in sandbox |

The Loader scans regular resource files into an immutable manifest with sizes and SHA-256 hashes.
Sandbox creation is lazy: no backend is created until the Executor needs the workspace. On first use,
`SandboxedWorkspaceRuntime` stages manifest resources, verifies hashes through the sandbox command channel,
and applies the caller-supplied `WorkspacePolicy` (file count/size limits, command timeout/output
limits, empty-by-default environment allowlist, and disabled network access). Commands receive exact
argv vectors—not shell strings—and results include exit status, bounded stdout/stderr, timing, and
truncation state.

If a caller allows `workspace.*` without providing a backend, the run ends with status `failed`
before the model is called. Default v2 output uses error code `GL-SKILL-WORKSPACE-FAILED`;
explicit v1 output uses `workspace_unavailable`. The deterministic demonstration in
[examples/workspace_demo.py](examples/workspace_demo.py) uses an in-memory backend so it can run
offline; it is **not** a production sandbox substitute. Production callers inject a real
`SandboxBackend` and normally construct its transport from `gl-sandbox` public primitives.

### Events, receipts, cancellation, and retries

Tool Runtime performs whole-batch admission preflight before dispatch. Every dispatched call emits
correlated `gl_skill.tool_call` and `gl_skill.tool_result` events and produces a typed receipt. A
failed call produces a failed receipt; GL Skill does not automatically retry it. Terminal events are
coupled to statuses: `final_response` means `succeeded`, `cancelled` means `cancelled`, and
`error` carries other failure statuses.

Pass an `asyncio.Event` for cooperative cancellation:

```python
cancellation_token = asyncio.Event()
cancellation_token.set()
async for item in client.run(request, cancellation_token=cancellation_token):
    if isinstance(item, SkillRunResult):
        assert item.status.value == "cancelled"
```

Limits on turns, tool calls, wall-clock time, and output size are set by `RunLimits`; workspace
resource/command bounds are separate in `WorkspacePolicy`.

### Deferred integrations

Deep Agents, remote lifecycle management, and bidirectional synchronization are deferred. AIP/GL
Connectors may initiate runs or supply external integrations, but Skill scripts and commands execute
only inside gl-sandbox.

The internal GLLM boundary is documented separately in
[runtime adapters](docs/runtime-adapters.md); it is never loaded by the core import surface.

## Optional workspace runtime

GL Skill never selects a sandbox provider or reads provider credentials. The application chooses
and configures its supported `gl-sandbox` backend, then passes a factory that returns its
provider-neutral `SandboxBackend`. `lazy_workspace_tool_runtime()` stores that factory without
constructing a backend or importing `gl_sandbox`; the first executor-admitted workspace call
initializes both exactly once. An instruction-only run, or a run with caller tools only, stays
sandbox-free.

Callers consume the public `GLSkillClient.run()` stream; the executor does not
expose a separate one-shot lifecycle.

The executor supplies an immutable `ToolContext` for every admitted call. Its request ID,
remaining deadline, cancellation view, and complete `workspace_policy` are authoritative; unknown
policy fields, invalid bounds, network access, and implicit environment inheritance fail closed.
The default environment allowlist is empty. The workspace layer passes only exact argv vectors to
the two public `gl-sandbox` primitives—no shell, provider-private helper, GNU command, or glob
expansion is required.

Lifecycle ownership is deliberately singular. Normal completion is terminated by executor cleanup.
If a public primitive is cancelled or its outer deadline expires, that primitive terminates its own
sandbox and the workspace runtime only discards the handle; executor cleanup then becomes a no-op.
This prevents a second termination of the same backend.

## Development

Open-format naming, metadata, provider reuse, and runtime-ownership decisions
are documented in [Agent Skills format compatibility](docs/agent-skills-compatibility.md).

```bash
make setup
make check
```

`make check` runs formatting/lint checks, strict typing, unit tests, contract-schema
validation, wheel construction, and the installed dependency-footprint and convergence
gates. Package-local checks use
the locked development environment; preparing it may require configured-index access.
The artifact gates create separate clean environments and resolve the wheel's declared
dependencies from the configured indexes, rather than reuse the development environment
or its lock. They need a usable dependency cache or index access, including private-index
credentials on a cold cache. A prepared development environment alone does not make
these gates offline or guarantee the same resolved dependency versions.

### Dependency-footprint gate

`scripts/check_dependency_footprint.py` verifies that the wheel contains both `gl_skill` and
`gl_skill/py.typed`; that its base runtime dependencies include core, GLLM, `jsonschema`, and `pyyaml`; that a
clean installed environment with declared dependencies can import `gl_skill`; and that doing so loads none of these forbidden
top-level modules:

- `aip_agents`, `aip_sdk`, `glaip_sdk`
- `gl_connectors`
- `hermes`
- `gllm_inference`
- `gl_sandbox`, `e2b_code_interpreter`, `opensandbox`, `boto3`, `aioboto3`
- `openai`, `anthropic`, `google.genai`, `google.generativeai`
- `cohere`, `groq`, `litellm`, `mistralai`, `ollama`, `vertexai`
- `skills_ref` (development-only format oracle)

GLLM inference with its `[openai]` extra is a required dependency; callers do not need to select
a separate GL Skill optional extra. The gate checks the declared dependency graph and verifies
that importing `gl_skill` keeps provider runtimes unloaded.

### Normalized error contracts

`BaseSkillError` and 16 concrete errors are available from `gl_skill.exceptions` and the package
root. They use the core `BaseError` constructor and registry, with stable `GL-SKILL-*` codes and
actionable messages. Loader error details use fixed, source-free text.
See [error contracts](docs/error-contracts.md) for the catalog and exception-handling guidance,
and [error codes](docs/error-codes.md) for the code index and stability policy.

The current modules are:

| Purpose | Module |
| --- | --- |
| Exception types | `gl_skill.exceptions` |
| Failure recording | `gl_skill.exceptions.error_handling.records` |
| Failure normalization and message selection | `gl_skill.exceptions.error_handling.failures` |
| Diagnostic profiles and context | `gl_skill.exceptions.error_handling.diagnostics` |
| Recovery contracts and integration protocols | `gl_skill.exceptions.error_handling.contracts` |

`SkillDiagnostic` validates bounded internal profiles and exports detached dictionaries
for core attachment. `SkillErrorContext` and its bind/get hooks provide explicit,
immutable context with task isolation and scope restoration. `SkillRecovery`,
`DiagnosticSanitizerProtocol` and `RecoveryAdvisorProtocol` define downstream
integration boundaries. Core `BaseError.recovery` carries retryability and upstream retry
delay; `SkillRecovery` carries supplemental action/reason/escalation guidance. Retry limits
and backoff remain caller policy. The recording service assigns `BaseError.reference_id`;
`SkillErrorContext` has no reference field. Sanitize or exclude sensitive data before constructing
diagnostics or attaching `debug_info`; validation only checks structure and bounds.

`DefaultDiagnosticSanitizer`, `select_failure_message`, `DefaultRecoveryAdvisor`, and
`normalize_failure` provide diagnostic sanitization, catalog messages, effect-aware recovery
decisions, and failure normalization. Internal diagnostic and context data are not automatically
included in public output. SDK failure logs carry the operation and normalized `GL-SKILL-*` code
without tracebacks, request IDs, or exception type names.

### Run output versions

Loader `SkillLoadResult.to_wire()` uses contract `2.0`; `to_model_text()` renders compact
`<skill_content>` or `<skill_error>` blocks for the model. Loader results have no version selector
or public recovery/reference fields.

Run streams (events, receipts, results) use contract `2.0` by default. Use `2.0` for new integrations to
receive normalized error codes and structured recovery guidance. Contract `1.0` is retained
for legacy compatibility; select `output_version="1.0"` only when an existing integration
requires the legacy output format.

The same selection rules apply to `GLSkillClient.run()` and `GLSkill.run()`:

| Run contract | How to select it | Error fields in run output |
| --- | --- | --- |
| `2.0` — default and recommended | Omit `output_version` or pass `output_version="2.0"` | `code`, `message`, `recovery`, and optional `reference_id` |
| `1.0` — legacy compatibility | Pass `output_version="1.0"` explicitly | `code`, `message`, `retryable`, and `details` |

The optional v2 `reference_id` is for the host application; model feedback never includes it.

By default, run errors use normalized `GL-SKILL-*` codes and catalog messages. Their `recovery`
object always includes `action`, `retryable`, and `escalate_when`. Retryable errors also include
`retry_after_seconds` and `max_attempts`.

Each stream uses one output version throughout. Unsupported `output_version` values raise
`ValueError` before any item is yielded, with the message:
`Unsupported output version. Use "1.0" or "2.0", then try again.`

Events and results serialize their stamped version through a no-argument `to_wire()` method.
Receipts also use their stamped version by default; passing a different version to
`to_wire(output_version=...)` raises the same error.

Both schema versions ship in `gl_skill/contracts/schemas`. See
[contracts](contracts/README.md) and [error contracts](docs/error-contracts.md).

Standalone `SkillError.to_wire()` and directly constructed `SkillRunRequestData`,
`ToolReceiptData`, `SkillRunEvent`, and `SkillRunResult` values also default to run 2.0. Pass
`output_version="1.0"` to standalone serialization or object construction when preserving legacy
output, including on receipts nested in a v1 result.

After a failed tool call, model feedback uses the run `2.0` error object by default, with
`reference_id` omitted. Explicit run `1.0` uses `{code, message, retryable}`. Both versions use
catalog messages and exclude the receipt's original message and `details`.

## Examples index

| Example | Purpose |
| --- | --- |
| [`hello_world.py`](examples/hello_world.py) | Canonical low-code `GLSkill.run()` call |
| [`quickstart_run.py`](examples/quickstart_run.py) | Deterministic explicit-composition `run()` call |
| [`streaming_run.py`](examples/streaming_run.py) | Ordered events followed by one terminal result |
| [`caller_tool.py`](examples/caller_tool.py) | Register a trusted callback and inspect its receipt |
| [`workspace_demo.py`](examples/workspace_demo.py) | Deterministic sandbox-policy demonstration using an in-memory backend |
| [`live_model/openai_smoke.py`](examples/live_model/openai_smoke.py) | Explicitly credential-gated real-model smoke test |
| [`loader_v2_failure.py`](examples/loader_v2_failure.py) | Loader 2.0 failure output and safe model text |
| [`run_v2_failure.py`](examples/run_v2_failure.py) | One run failure with default 2.0 output and explicit 1.0 compatibility |
