Skip to main content

kirby-ai

Kirby's interface to kirby-medialib's generation endpoints. Published to PyPI as kirby-ai and to the ProductBinder index; it carries no keys — seats are named here, provider keys live in medialib.

One client, replacing four independently written medialib clients in kirby-api.

Seats

A call site names a seat, never a model:

client.generate(seat="combat-pick", prompt="...")

A seat is a row in kirby-medialib, not a file this package reads (medialib-api spec 2026-09-16): medialib resolves the provider and model server-side, so the client only ever sends the seat's name. seeds/seats.dev.json (local stack, Ollama-backed) and seeds/seats.prod.json (Hetzner, cloud providers only) are the seeds for that table, applied by medialib-api/scripts/seed_seats.py; they are not consulted at request time. The two files define the same seat names.

{
  "tenant": "kirby",
  "seats": {
    "combat-pick":    {"provider": "ollama", "model": "kirby-mythos:latest"},
    "combat-review":  {"provider": "ollama", "model": "kirby-mythos:latest"},
    "campaign-prose": {"provider": "ollama", "model": "kirby:latest", "vision": true}
  }
}

generate() has no model or provider argument, and a test asserts their absence. The wire body carries seat and nothing that names a provider. Overriding a seat means adding a seat -- in medialib, now, not here.

There are no defaults. An unnamed seat raises. A tool that silently picks a model has signed a claim on your behalf — and Kirby has the receipt: a provider pinned inside one module sent a local-model fight to a metered API for four months with nothing in the log to say so.

Choosing an action

ModelChooser fills the seat kirby_combat.loop.Chooser leaves open: the engine renders the Phase as a Brief, this sends it, and takes back an action_id. One pass.

DeliberatingChooser adds a second — propose → review → execute or re-propose — because "on the menu" is a legality test, not a sanity test. The O.K. Corral keeps producing legal absurdities: a marshal who cannot see an enemy spending his Phase shooting a boarding house, five men with revolvers trying to level a saloon they could not dent. A reviewer holding the proposal catches those without anyone having to anticipate them as a rule.

DeliberatingChooser(client, propose_seat="combat-pick",
                    review_seat="combat-review", rounds=1)

rounds counts re-proposals: 0 is a single pass and behaves exactly like ModelChooser, so the two can be compared on one benchmark by changing one number. The reviewer never picks — it approves, or sends the proposal back with a stated objection, and only the objection travels back. A reviewer that cannot answer never vetoes: a dead network, prose instead of JSON, or an unrecognised verdict all resolve to execute, recorded in notes.

Two seats, not one, because reviewing is not the same job as proposing and must not become the same model by accident.

A model's answer can fail to reach any chooser — a dead transport, prose instead of JSON, an id off the menu — and by default every one of these quietly substitutes FirstLegalChooser and records why in .notes, so a benchmark keeps running rather than dying on one malformed reply. Pass fallback="raise" to opt out of that: ModelChooser, DeliberatingChooser and RoleCouncil all raise kirby_ai.NoChoice instead of substituting, carrying the actor, the offered action_ids, and the reason the model could not be honoured. This is for a harness that must be able to say "I don't know" rather than act on an unreviewed silent standby — a caller reading .notes after the fact is not the same as one deciding, per Phase, whether to proceed. The default (fallback=None, or any chooser instance) is unchanged.

Intent

A seat that decides from scratch every Phase has no memory: it closes to melee, then shoots, then closes again, and a reader watching the log cannot tell a change of mind from a change of circumstance. IntentChooser wraps another chooser and gives each actor a standing plan — an objective and a target, formed from what it actually chose — kept across Phases until something happens worth rethinking for: the target goes down or out of sight, the actor takes a hit worth a quarter of its current STUN, its stance changes, nothing on the menu serves the plan any more, or the commitment runs out. The commitment is measured in turns, not Phases, and how many is a property of the fighter: the role the engine classifies it as, moved by the psychological complications the player paid points for.

from kirby_ai import IntentChooser, ModelChooser

seat = ModelChooser(client, seat="combat-pick")
chooser = IntentChooser(seat, commitment_turns=2)

The plan advises, it does not decide. The inner seat still picks; the live plan reaches that seat through the engine — Brief.render(extra_doctrine=...) puts it in the engine's own doctrine section, under the engine's heading and the engine's suppression, so doctrine=False and KIRBY_BRIEF_NO_DOCTRINE remove the plan along with everything else advisory. Nothing here splices text into a rendered page.

That advisory shape is the one deliberate difference from the kirby-api layer this was ported from, where a live plan continued without a model call at all: the intent layer picked the serving action itself. The price is one model call per Phase where the api made none while a plan held — for a fight of any length that is most of the calls, so IntentChooser today buys coherence and costs money, where the api's bought both. Restoring the saving is a follow-on (continue_without_call), not something this layer decides on its own.

IntentChooser installs itself on the inner chooser: it sets inner.doctrine_lines, chaining any source already there rather than replacing it. That is a side effect on an object the caller owns — a wrapper is built after the thing it wraps, so there is no other moment to do it — and two IntentChoosers over one seat would stack their lines. chooser.renders_intent says whether the plan is reaching the page at all.

fallback="raise" passes straight through: a harness that asked not to be guessed for still hears NoChoice.

What the port changed

dropped the intent table and its event lane (api rows), the character_tactic_profile base, the construct damage-overlay id translation, the per-combatant morale_state and with it all rage suppression, and the Tactics/INT notice roll (a Chooser rolls no dice — notice is the caller's callable and, given none, nothing is noticed)
added throw and throw_object now imply an objective (ENGAGE_MELEE and KEEP_RANGE); the api's kind map omitted both although its preference table already served them. A STUNNED actor now abandons its plan: is_significant_hit is asked with stunned=, folded off the engine's event log by statuses_for, which the api read from its own rows
rungs health is kirby_combat.classify_health, the engine's one door. The 0.25 significant-hit fraction stays here: it is planning policy and no rule fixes it

Doctrine

TacticsDoctrine puts the engine's own tactic catalogue on the page as offers a seat can return. It takes a PhaseSituation and gives back ranked lines shaped "<kind> -> [<action_id>] against <name>"; install it with ModelChooser(..., doctrine_lines=TacticsDoctrine()) and they reach the reader through Brief.render(extra_doctrine=...), in the engine's own doctrine section, under the engine's suppression. Nothing here splices text into a rendered page.

from kirby_ai import ModelChooser, TacticsDoctrine

seat = ModelChooser(client, seat="combat-pick", doctrine_lines=TacticsDoctrine())

What it calls in the engine: tactics_for (the catalogue, ranked — every tactic, its Plan and its Basis are kirby-combat's), PhaseSituation. tactical_situation and ordered_menu, and Brief.render(extra_doctrine=...) for delivery. The matching rule is TacticChooser's, step for step: steps are tried in order, an offer of the right kind at a different man does not count, and a tactic the menu cannot serve is skipped. Advice that contradicts the engine's own deterministic seat would be a contradiction on one page.

Why there is anything to add, and only that. Brief.doctrine already renders name -> kind: summary. A kind is not returnable — the seat must answer with an action_id — and sixteen of the catalogue's tactics name a target_id that never reaches the page, so with two enemies in front of you the engine's line says "close and strike" and does not say who. These lines are continuations of the engine's, directly below them under the same heading: they add the offer and the man and never restate the tactic's name, summary or rationale. Two tactics wanting one offer say it once. test_every_tactic_is_named_once_on_the_page renders the real page through Brief.render(extra_doctrine=...) and counts, and test_it_advises_what_the_engines_own_seat_would_take pins the first line to TacticChooser().choose() on the same Phase.

A tactic that raises is an engine defect and arrives as one. Nothing here catches; a caught exception would read as "no doctrine applies", forever.

What the port changed

not ported the catalogue itself. All twenty of the api's tactics are already in kirby_combat/tactics/catalog/ — terrain-aware ones included, and repaired there (smash_cover named a man where the offer is keyed by a wall; take_cover_when_hurt kept its own copy of the health ladder). A second copy here would be a second answer. Covered by the api's tests/combat/test_tactics_catalog.py (all of it — smash_cover / fight_from_cover / keep_range / close_and_strike / exploit_hazards) and test_tactics.py::test_bait_enraged_*, ::test_exploit_susceptibility_*, ::test_focus_fire_*, ::test_presence_attack_*, now the engine's tests/test_tactic_catalogue.py
dropped llm_selector.py — a rules library asking a service which tactic to use and falling back to priority on any error. The engine deleted it deliberately (select_tactic: choosing among what applies is the consumer's job), and the consumer is the seat, which picks an offer rather than a tactic name. Covered by test_tactics.py::test_select_tactic_with_llm_mock, ::test_select_tactic_llm_fallback_on_error, ::test_compare_selectors_runs_each_model, ::test_compare_selectors_handles_per_model_failure
dropped the deterministic priority selector (test_tactics.py::test_select_tactic_deterministic_priority_when_no_llm) — that is tactics_for, in the engine
dropped the role split and its rowsTacticProfileRow / TacticDefinitionRow, per-entry weights, offensive/defensive lists on StepSituation. Rows are kirby-api's and the api's tables go with the harness. Covered by test_tactics_role_split.py (all of it)
dropped seven tactic definitions that existed only as seeded rows, with no class behind them and no engine counterpart by name: finish_dying, target_stunned, defensive_recover, separate_grouped, silence_caster, lure_to_cover (alembic 20260504_0010_tactic_catalog_expansion) and slam_velocity_attack (20260504_0011). Advice is only worth rendering if something decides it applies, and nothing in code ever did for these. Covered by tests/combat/test_situation_builder.py::test_build_situation_active_profile_resolves, which names target_stunned, silence_caster and separate_grouped in its comment and asserts only that the original four profile entries still land; finish_dying, defensive_recover, lure_to_cover and slam_velocity_attack had no api test at all. lure_to_cover and separate_grouped are positional advice the engine's catalogue lacks — listed under engine gaps, deliberately not implemented here
dropped the tactic's Basis, from the rendered line. The engine's own doctrine line is where a tactic explains itself; quoting its citation down here was one more restatement

The calculator seat

EVChooser is a Chooser that prices every offer and takes the best: P(hit) x the STUN the engine's resolver gets through, averaged over every roll the dice can produce. It is kirby-api's sim_ai.py expected-value pick, ported.

from kirby_ai import EVChooser

seat = EVChooser()          # .picks records the number behind every decision

No fallback. A Phase where nothing can be priced raises NoChoice rather than taking the first legal offer. A calculator that quietly stops calculating is worse than one that stops, because the fight goes on looking decided.

The to-hit number is the engine's. chance_to_hit asks resolve_to_hit (or resolve_mental_to_hit for a mental attack) once per 3d6 total, 3 through 18, and weights each answer by how often that total comes up. Both functions are public and pure — they read their inputs, spend no END, emit no event and touch no session — and both settle hit-or-miss independently of the dice handed in. So Combat Skill Levels, the No Range Modifier Advantage (6E1 p.346) and mental combat's range-free roll (6E1 p.105) are the engine's answers. The seat hands over the distance; what it costs is not its to say.

What it calls in the engine: resolve_to_hit / resolve_mental_to_hit, compute_defense (which also knows Armor Piercing, defense items and AVAD), compute_damage, killing_damage / normal_damage with the neutral body — the same four damage primitives kirby_combat.critique asks, for the same reason — and PhaseSituation.ordered_menu so scenery sorts last.

The probabilities are counted, not typed. chance_of_rolling(n) is P(3d6 <= n) enumerated off the die: 0.5 at 10, because half of 216 outcomes are 10 or under. That distribution is the only thing here the engine does not own. The killing ½d6 STUN multiplier is rolled through the engine the same way, once per face.

Ties and futility. Equal values keep the first offer in ordered_menu — no die is rolled to separate them — and a menu where every attack prices at zero raises NoChoice rather than returning one: critique would file every one of those picks as futile.

What the port changed

dropped the 17-entry 3d6 table — derived from the die instead (api test_sim_ai.py::test_hit_probability_known_values)
dropped the to-hit target number itself. The api computed ocv + 11 - dcv and so did this module's first draft, which omitted CSLs, charged a No Range Modifier power the full range penalty and charged mental attacks a penalty they never pay. resolve_to_hit / resolve_mental_to_hit answer all three (::test_expected_damage_no_abort_basic covered the api's version)
dropped _avg_damage's 3.5-a-die and its 1.83 killing multiplier, and _defense_for's pick-a-pool-by-type — the engine's resolver answers both (::test_expected_damage_no_abort_basic, ::test_md_reduces_mental_blast_damage)
dropped the SPD_PHASES chart — kirby_combat.tables.segments_for_spd (::test_abort_at_segment_12_wraps_to_next_turn)
dropped DODGE_DCV_BONUS / SET_OCV_BONUSDodge.dcv_bonus and actions/set_action.py (::test_expected_damage_with_dodge_lower_than_no_abort)
dropped CoverFeature / cover_at / cover_ocv_penaltyscene/cover.py computes cover from the real map and cover_ocv_modifier prices it (::test_cover_at_picks_best_within_adjacency, ::test_cover_at_returns_empty_when_no_adjacent_cover, ::test_cover_ocv_penalty_thresholds)
dropped distance / step_toward / step_awayscene/placement.move_toward and scene/geometry.py, which also know what is in the way (::test_distance_3d, ::test_step_toward_clamps_at_destination, ::test_step_away_moves_directly_away)
dropped int_roll and its MASTERFUL/SOLID/MUDDLED/BAD-CALL tiers — a Chooser rolls no dice, and the roll target is the engine's (hero_view's skill roll target, kirby_cost.engine.rolls). Same call IntentChooser made about the notice roll (::test_int_roll_target_calculation, ::test_int_roll_tier_thresholds)
dropped maybe_set's 0.30 hit-probability threshold — an invented number with no page behind it, and a Set is an offer on the menu like any other (::test_maybe_set_triggers_at_low_hit_probability, ::test_maybe_set_skipped_when_hit_chance_already_high, ::test_maybe_set_skipped_if_already_setting)
dropped the whole abort seatpick_best_defense, maybe_abort_to_defense, AbortDecision, the owned-martial-maneuver reading, and with it Block's full negation on a successful block. An abort is reactive and the Chooser protocol decides a Phase; there is nowhere to seat it yet (see the gap below). Covered by ::test_pick_best_defense_*, ::test_abort_*, ::test_expected_damage_with_block_full_negation_on_success, ::test_dodge_does_nothing_against_mental_attack, ::test_block_returns_inf_against_mental_attack
dropped _DISABLING_THREAT_STUN — hand-set "threat-equivalent STUN" for Entangle, Drain, Flash, Mind Control and the rest, with no page behind any of them. Nothing here invents a number, so a 0-STUN disabling attack prices at 0 and this seat will never pick one. That is a real behaviour change and the gap below is where it belongs (::test_is_disabling_attack_recognises_common_xmlids, ::test_disabling_threat_stun_values, ::test_expected_damage_for_entangle_returns_threat_stun_not_zero, ::test_pick_best_defense_aborts_against_entangle, ::test_flashdef_reduces_flash_threat)
dropped the six-xmlid "is this mental" list — the engine puts damage_type / defense_type on the power (::test_pick_best_defense_returns_none_for_mental_attack)
added the distance. The api's EV took an ocv_modifier from its caller and never measured; the Phase knows how far everyone is, so distances_m is handed to resolve_to_hit, which decides what it costs. A shot priced without it is priced at point blank from across the street. Pinned by test_the_engines_range_rule_is_honoured

Engine gaps this found

  • No probability primitive. kirby-combat resolves a roll, but nothing there can say what a roll is worth before it is made, and nothing can say what an attack is expected to do — critique prices only the luckiest roll. chance_to_hit and expected_stun are priced-before-rolled and belong with the rules; asking the engine 16 times per offer is what a consumer has to do without them.
  • No mental / physical to-hit dispatch. actions/base.py calls resolve_to_hit; nothing routes a mental power to resolve_mental_to_hit. So a consumer picks the door off damage_type / defense_type, which is a rule-shaped question it should not be answering.
  • No public tactic-matching door. TacticChooser.choose is the only place that turns a Plan into an offer on the menu; anything else that wants the same answer copies it, as TacticsDoctrine._offer_for does (pinned by a parity test). The engine has corrected that logic twice already.
  • No public "who does this action_id mean" door. critique._target_of is private, so EVChooser._target is a second copy that will not follow the engine when it widens the lookup.
  • Two positional tactics the catalogue lacks. kirby-api seeded lure_to_cover ("if cover is within ½ Move, take it before attacking") and separate_grouped ("when enemies cluster, scatter them with high-knockback attacks") as rows, with no code behind them. Neither has an engine counterpart; both are advice about ground the engine already models (scene/cover.py, knockback), and both belong in kirby_combat/tactics/ catalog/ if they are wanted at all — never here.
  • No threat equivalence for status attacks. An Entangle, a Flash or a Mind Control does 0 STUN and ends fights. Until the engine can price one, any expected-value seat will ignore them.
  • No reactive seat. Aborting to a Dodge or a Block is a decision, made between Phases, and the Chooser protocol has no room for it. The api's defender-side EV picker has nowhere to go until there is one.

Narrating a fight

Narrator speaks one line in a fighter's voice over the events of a Phase — or says nothing, which is most Phases. The events are the engine's own; the policy is kirby-api's, ported.

from kirby_ai import Narrator

narrator = Narrator(client, seat="combat-narrate", significance=0.5)
line = narrator.narrate(events, situation)   # str | None

Nothing here stores anything. narrate returns the text and the harness writes it — in kirby-api, to its own combat_narration table keyed by (session_id, sequence). There is no narration event: the engine's CombatEvent union is closed and narration is not a state change.

One Narrator per fight. The throttle (one line per fighter per Turn) is state on the narrator. A harness that builds a fresh Narrator per request loses that history and every significant moment speaks again; keep one per session for the fight's life, as with IntentChooser. Also note the PASS door is ported verbatim: a reply that is literally "Pass!" is read as the character choosing silence.

The gate is pure and runs first. Each event is priced on a 0–1 dial and the Phase is silent unless something clears significance. The moments, most dramatic first: the fight ending (1.0), a man going down (0.9), a man left at death's door (0.8), a barrier destroyed (0.7), the scene turning on somebody (0.6), a man visibly badly hurt (0.6), a barrier going up (0.4). A wound is also priced as the fraction of the man it was taken from, so twelve STUN off a thug is a moment and twelve off a brick is not. None of these numbers is a rule — no page of 6E says when a man speaks — so they are policy here, and significance overrides all of them at once.

Vitals are read from VitalsChanged, which is where they now are: the engine's A7 work made the change itself an event, so a fight replayed from its log is narrated the same way as the fight that ran. Where the man stands after the blow comes from classify_health, the engine's one door, rather than from a before/after pair this package snapshotted.

One line per man per turn, kept on the narrator rather than queried back out of rows, with the api's single exception: the fight-ending line is never eaten because the winner spoke earlier in the turn.

None is a decision, never a failure. It means exactly three things: below significance, already spoke this turn, or the character chose silence (the seat's PASS). A seat that errors, or answers with nothing at all, raises — this returns no placeholder text.

What the port changed

dropped the combat_event row it wrote, EventRepository, the sequence bump and the savepoint around it, and the SOLILOQUY_EVENT_TYPE lane on the log. The harness stores the text (test_soliloquy.py::test_maybe_narrate_emits_event_row, ::test_rewind_survives_interleaved_soliloquy_rows)
dropped the throttle's query_turn_boundary_seq walking SegmentAdvanced rows backwards for the last from_segment == 12, and _spoke_this_turn over soliloquy rows. PhaseSituation.turn is the boundary and the narrator remembers who spoke (::test_throttle_one_soliloquy_per_actor_per_turn)
dropped the five before/after snapshot arguments the driver took around _resolve (combat_started_before, target_stun_before, target_health_before/after, construct_effects_fired) — every one of them existed because the state change was not in the log. It is now (::test_ko_when_stun_crosses_zero, ::test_low_health_crossing_when_class_worsens)
dropped first_blood. It asks whether this fight has drawn blood before, which one Phase's events cannot answer and which no engine event records (::test_first_blood_on_first_damaging_hit, ::test_no_first_blood_when_combat_already_started, ::test_no_first_blood_without_damage)
dropped drowning_start and drowning_already_narrated — the api's per-segment construct effects with outcome == "suffocating", and the once-per-dunk memory read off its own rows. The engine's nearest event is EnvironmentalTriggered, which is scored as a hazard without knowing that drowning is one long moment (::test_drowning_start_first_suffocating_tick, ::test_no_drowning_when_already_suffocating, ::test_no_drowning_without_suffocating_outcome)
dropped the three payload shapes _construct_destroyed had to know — top-level, nested under construct_damage, and the AoE list under construct_hits. ConstructDamaged.destroyed is one field (::test_construct_destroyed_top_level, ::test_construct_destroyed_nested_in_construct_damage, ::test_construct_destroyed_in_construct_hits_list)
dropped victory_after as a caller's flag, and with it the driver's _actor_won side arithmetic. SessionEnded is in the log (::test_victory_trigger_when_victory_after_true, ::test_actor_won_loser_acting_and_mutual_ko, ::test_driver_step_victory_narrated_by_winner)
dropped OllamaConfig, the bearer token as an argument and the settings.kirby_medialib_api_url read — the client is constructed by the caller, which is why this package is testable without a web application (::test_maybe_narrate_no_bearer_skips_the_seam)
dropped profile_name — the api's character_tactic_profile row in the voice context. The complications stay (::test_driver_step_wall_destruction_emits_soliloquy)
reversed failure is raised, not swallowed. kirby-api wrapped every touch and returned None on any exception, because narration was decoration the combat loop must not care about. A harness that STORES the line cannot tell "he said nothing" from "medialib is down" that way (::test_maybe_narrate_seam_failure_never_raises)
kept the system prompt and the PASS door verbatim, the priority order of the moments, and the one-line-per-man-per-turn throttle with its victory exemption
rungs health is classify_health and the sheet is complications_of — the engine's doors, where the api had its own _classify_health snapshots and a complications_for(db, character_id) query

Engine gaps this found

  • A wound has no "worse than before" in the log. VitalsChanged carries the delta and classify_health reads where a man stands now, but nothing says he crossed a rung on this blow — the api knew because it snapshotted the class either side of _resolve. A consumer that wants the crossing re-derives it by keeping its own before-picture, which is the thing the event was meant to end.
  • Drowning is not a state. EnvironmentalTriggered fires every segment a man is under water and carries a free-form effect dict; there is no event for starting to suffocate and no status for being in it, so "he just began to drown" cannot be told from "he is still drowning" without a consumer's own memory.
  • No first blow. Nothing records that a fight has gone live. SessionStarted is setup and the first damaging VitalsChanged is only recognisable as the first if you have read the whole log.

Layering

layer owns
kirby-medialib providers, credentials, execution, and the seat rows themselves — shared with ProductBinder
kirby-ai the client, parsing, provenance
kirby-api HTTP serving, persistence. Names a seat, never a model.
kirby-combat rules. Knows nothing about any of this.

combat-pick and campaign-prose are still Kirby's vocabulary — they are just rows scoped to the kirby tenant in medialib now, not a file this package loads.

What it owns, and what it does not

Rules and rows belong to their own layers, and nothing here reads either.

Prompts live here, which this file used to deny. The claim was that "a prompt is domain knowledge — callers hand this package finished strings", and there is no caller that can. kirby-combat forbids the word: prompt is in the banned-term list its vocabulary guard fails the build on, alongside llm and ollama, because the engine models choosing and stays neutral about what implements a choice — which is what lets a fight run with no dependencies at all. And the only callers that would hand the strings over are the scripts in this repository.

So a prompt for the combat-pick seat has nowhere else legal to be, and no consumer outside this package: the seat is only ever sent by the client here. The test is the same one that keeps the deliberation harness here rather than in a package of its own — if nothing outside kirby-ai can ever use it, it belongs to kirby-ai. A charter the code has broken since before it was written, in the one place the code has no alternative, is a charter that is wrong.

What that costs is real and worth stating: someone tuning a combat prompt is editing a transport package, and a HERO rules change can want a prompt change in a repo that knows nothing about HERO. The seat names are the seam that keeps this survivable — combat-pick, combat-review, combat-atmosphere say who a prompt is for without this package having to understand it.

Dependencies

httpx, and nothing else. tests/test_leaf.py enforces that, and enforces that nothing here imports kirby.settings — the base URL and bearer token are passed in, which is what makes the package usable from a script and testable without a web application.

Release files for kirby-ai 0.3.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for kirby-ai 0.3.1
File Size Uploaded
kirby_ai-0.3.1.tar.gz 101.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for kirby-ai 0.3.1
File Interpreter ABI Platform
kirby_ai-0.3.1-py3-none-any.whl Python 3 none any Details

Total release size: 176.8 kB

Release files / kirby_ai-0.3.1.tar.gz

Download URL kirby_ai-0.3.1.tar.gz
Size 101.8 kB
Tags Source
SHA-256 checksum
How to use checksums
5217c32650dbb8201dde0554eb28b07eaca679b16ab419d3bc0f193810ff6a12
BLAKE2b-256 checksum
How to use checksums
e31afaa57fabfe93110671f884188fbbe5f18462ce415e86d587ac5351a117ae
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / kirby_ai-0.3.1-py3-none-any.whl

Download URL kirby_ai-0.3.1-py3-none-any.whl
Size 75.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
79cc26537802040b149859cc99f28a195f3852b71ebb02770703fdf75ce0c943
BLAKE2b-256 checksum
How to use checksums
54053f8c126cf346e53101e9148ee471726d662472cdf866d2bf77bb9503de2a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

This release

0.3.1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page