yggdrasil.loki.agent¶
agent ¶
Loki — the global yggdrasil agent.
Loki is one agent that adapts to wherever it runs. It detects the backends
it can reach (:mod:yggdrasil.loki.capability), acts as a token /
credential provider for them (chiefly Databricks — when a session is
present Loki hands its authenticated client to whatever it drives), and
dispatches :class:~yggdrasil.loki.skill.LokiSkill actions. The CLI
(ygg loki) is a thin shell over this object.
from yggdrasil.loki import Loki
loki = Loki.current()
loki.card() # who am I + what can I reach
loki.databricks # the live DatabricksClient, or None
loki.run("genie", space="01ef…", question="revenue by region")
ActStep
dataclass
¶
ActResult
dataclass
¶
Loki ¶
The global yggdrasil agent — capability-aware, token-providing.
agent_id
property
¶
Stable int64 id derived from user@host (xxhash, not crypto); cached.
databricks
property
¶
The authenticated Databricks client when a session is present.
This is Loki acting as a token provider: skills and downstream
code take this client to reach Databricks service endpoints (SQL,
Genie, jobs, serving, …) under the agent's resolved credentials.
Returns None when no Databricks session is detected.
aws
property
¶
The configured :class:~yggdrasil.aws.AWSClient when AWS is reachable.
Loki as an AWS token provider — the AWS skill fleet rides this
client (its resolved credentials / region / role). None when no
AWS session is detected.
backends ¶
Detected backends (cached; pass refresh=True to re-sniff).
load_specialists ¶
Import the specialized skill fleets for every reachable backend.
Databricks problems get the databricks-* skills, AWS problems the
aws-* skills — registered only when their backend is detected, so
ygg loki skills shows the fleet that actually applies here.
Returns the backends whose fleet loaded.
token_info ¶
Non-secret summary of the Databricks credentials Loki provides.
whoami ¶
The Databricks user, if reachable. probe allows one network call.
engines ¶
Every known reasoning engine (call .available() to filter).
available_engines ¶
Reachable engines (name → instance), availability probed in parallel.
Several engines gate on a network round-trip — the Ollama liveness
probe, the Databricks backend check — so probing them one after another
stacks up their latencies on the startup path. Fanning the
available() checks across a small thread pool collapses that to the
slowest single probe. Each engine memoizes its own result, so this also
warms the caches that later serial available() calls (the status
line, :meth:engine, :meth:select) reuse for free.
engine ¶
Resolve a reasoning engine by name, or the best available one.
select ¶
select(
text: "Optional[str]" = None,
*,
tier: "Optional[str]" = None,
base: "Optional[str]" = None,
confirm: "Optional[Callable[[TokenEngine, Optional[str]], bool]]" = None
) -> "Optional[TokenEngine]"
Resource-aware engine choice: keep light work cheap, escalate heavy work to a capable remote — asking first.
Two axes drive the pick:
- Complexity — an explicit
tier(deep→ heavy), else the prompt itself (long or reasoning-heavy text → heavy). - Resources — a CUDA GPU, or enough CPU + RAM (≥ 4 cores, ≥ 8 GB), decides whether this box can comfortably run a local model.
A session pins a base provider, but complexity moves the choice both ways:
- remote → local (demote): light work on a capable box drops to a free local model even when the base is a remote API — saving money, silently (no downside to ask about).
- local → remote (escalate): heavy work climbs to the most capable
remote (the base remote if it is one). When this means switching
from a free local model up to a paid remote one,
confirm(engine, model)is asked first; a falsy answer keeps the work on the cheap/local path.
With no local engine the base simply stands; with no remote the local
engine carries even heavy work. Returns an available engine, or
None when nothing is reachable.
can_run_local ¶
Whether this box can comfortably host a local model — cached.
Wraps :func:yggdrasil.loki.resources.can_run_local but memoizes the
result for the process: the probe imports torch to check for a CUDA
GPU (slow on the first call, on a box that has it), and engine selection
asks this on every turn. Hardware doesn't change within a session.
bootstrap_local ¶
bootstrap_local(
*,
model: "Optional[str]" = None,
pull: bool = True,
on_progress: "Optional[Callable[[dict[str, Any]], None]]" = None
) -> dict[str, Any]
Ready a free local reasoning engine, lazily installing on demand.
Prefers a reachable Ollama server — ensures the model sized to this
workstation (the more RAM/GPU, the larger the default) is pulled, only
if missing. Falls back to the HF transformers engine (weights
lazy-download on first use). When neither is present, returns what to
install. This is the "free local brain" entry point — sized to the box,
smart enough for basic setup/config, and able to hand heavier work up to
a remote model.
reason ¶
reason(
prompt: str,
*,
system: "Optional[str]" = None,
engine: "Optional[str]" = None,
tier: "Optional[str]" = None,
base: "Optional[str]" = None,
confirm: "Optional[Callable[[TokenEngine, Optional[str]], bool]]" = None,
**options: Any
) -> str
Reason about prompt with the best (or named) engine → reply text.
tier ("fast" / "deep") forces the model tier; the default
(None) lets the engine pick adaptively from the prompt. A pinned
engine is used as-is; otherwise the choice is resource-aware
(:meth:select), sticking to the session base and asking
confirm before escalating to a paid remote model.
reason_stream ¶
reason_stream(
prompt: str,
*,
system: "Optional[str]" = None,
engine: "Optional[str]" = None,
tier: "Optional[str]" = None,
base: "Optional[str]" = None,
confirm: "Optional[Callable[[TokenEngine, Optional[str]], bool]]" = None,
**options: Any
) -> "Iterator[str]"
Stream a reply to prompt — yields text chunks as they arrive.
Same engine/tier resolution as :meth:reason, but live: the chosen
engine streams token deltas so the terminal prints them as they come.
act ¶
act(
task: str,
*,
root: str = ".",
engine: "Optional[str]" = None,
tier: "Optional[str]" = None,
max_steps: int = 12,
read_only: bool = False,
allow_shell: bool = False,
allow_web: bool = False,
confirm: "Optional[Callable[[str], bool]]" = None,
toolbox: "Optional[Toolbox]" = None,
on_think: "Optional[Callable[[int], None]]" = None,
on_step: "Optional[Callable[[dict[str, Any]], None]]" = None
) -> dict[str, Any]
Pursue task autonomously: discover, decide, and modify files.
This is Loki acting on its own — the reason→act→observe loop. The
agent's engine plans against a tool catalog (filesystem discovery +
edits, optionally a shell), emits one JSON tool call per turn,
and Loki runs it and feeds the observation back, until the engine
declares it's done or max_steps is hit. The tools are confined
to root (the working tree the agent was pointed at).
tier pins the model tier for every turn; left None the engine
adapts per turn — cheap early scouting turns, the capable model once
the transcript (and the reasoning) grows.
Returns a transcript: the resolved engine, every step (its
thought/tool/args/observation), the final answer,
whether it completed, and the files_changed list. Pass
on_step to stream each completed turn, and on_think(n) to learn
when turn n is about to call the (slow) model — the CLI uses the pair
to keep a live spinner + step-budget bar running through the otherwise
silent reasoning between tool calls.
delegate ¶
delegate(
tasks: list[str],
*,
root: str = ".",
engine: "Optional[str]" = None,
tier: "Optional[str]" = None,
max_steps: int = 8,
allow_web: bool = True,
allow_shell: bool = False,
read_only: bool = False,
timeout: "Optional[float]" = None,
on_update: "Optional[Callable[[list[Any]], None]]" = None
) -> list[dict[str, Any]]
Fan tasks out to background process agents and wait for them.
Each task runs as its own ygg loki do subprocess (an isolated act
loop), so independent work proceeds in parallel while Loki monitors them
— its autonomy multiplier. Returns one summary row per agent (status,
elapsed, answer, files_changed). on_update(agents) streams live
progress (the CLI renders the dashboard from it).
decompose ¶
decompose(
goal: str,
*,
engine: "Optional[str]" = None,
tier: "Optional[str]" = None,
max_tasks: int = 6
) -> list[str]
Break goal into independent subtasks an agent fleet can run in parallel.
Asks the reasoning engine for a JSON array of self-contained tasks
(parallel-safe — no ordering between them). Returns the parsed list,
capped at max_tasks; falls back to [goal] if nothing parses.
plan ¶
Classify a request into a structured :class:AgentPlan.
Loki reasoning, optimized: rather than throwing every prompt at one
engine, it classifies the problem — a category and solution
action (answer / act on files / fetch tabular / ask Genie), the
persona to embody (data engineer, analyst, software engineer,
trader, confessor, companion, …), the skills likely required, and
whether to isolate the work to a specialist (the "databricks on
databricks" scheme). Returns an :class:AgentPlan (mapping-compatible).
classify_data ¶
Global context: is this request data- or time-series-shaped?
Drives the data path — a positive classification routes a sourced
request to tabular fetching + caching (:class:TabularSkill) instead
of a plain page fetch. Returns {"data", "timeseries", "why"}.
specialist ¶
A specialized agent to isolate a category of work, or None.
"databricks" resolves the workspace-bound
:class:~yggdrasil.databricks.loki.DatabricksLoki when the SDK and a
session are present; otherwise falls back to self.
Cached per name: the REPL asks for the same specialist on every databricks turn, and the resolution (import + singleton lookup + backend check) is stable for the process.
run ¶
Dispatch skill skill_name with kwargs, using self as the provider.
The first parameter is skill_name (not name) so a skill that
itself takes a name= kwarg — e.g. scaffold(name=…) — can be
dispatched as loki.run("scaffold", name="acme") without a clash.
card ¶
Everything Loki knows about itself — identity, reach, skills.