devkit

    Small helper layer for Posoco extension authors

    posoco
    devkit
    extension
    logger
    Download zip
    Author
    Version
    0.4.0
    License
    Apache-2.0
    Last updated
    10 hours ago
    Downloads
    6

    Dependencies

    BusSubscriber

    pub(open) trait BusSubscriber {
    fn on_bus_event(Self, event : BusEvent) -> Unit
    }

    Subscriber contract: receive every event, ignore what you do not understand. One method, no defaults — a subscriber exists to react.

    Logger

    pub(open) trait Logger {
    fn log(Self, event : LogEvent) -> Unit
    }

    ModelMetricsSource

    pub(open) trait ModelMetricsSource {
    async fn read(Self) -> Result[Array[MetricsPoint], String]
    }

    A source of authoritative external model metrics. Implementations live in provider adapter extensions, where the vendor/community knowledge resides; consumers depend only on this trait. read returning Err means the metrics are unavailable — never a fallback estimate.

    PricingSource

    pub(open) trait PricingSource {
    fn phase(Self) -> PricingPhase?
    }

    A source of authoritative provider pricing phases. Implementations live in provider adapter extensions, where the vendor rule resides; consumers (model catalogs) depend only on this trait. phase returning None means no pricing is known — never a fallback estimate.

    QuotaSource

    pub(open) trait QuotaSource {
    async fn read(Self) -> Result[Array[QuotaReading], String]
    }

    A source of authoritative provider quota readings. Implementations live in provider adapter extensions, where the credentials and base URLs the probe needs already reside; consumers (rate-limit guards, status bars) depend only on this trait. read returning Err means the reading is unavailable — never a fallback estimate. Window labels are provider-owned opaque strings (examples seen: "5h", "weekly", "balance"); devkit defines no label vocabulary and never interprets labels.

    BusEvent

    pub(all) struct BusEvent {
    source : String
    topic : String
    data : Json
    } derive(
    Debug
    )

    One notification: source identifies the publisher (extension id), topic is the shared convention namespace, and data is the topic-defined payload.

    BusEvent::to_repr

    EventBus

    pub struct EventBus {
    subscribers : Array[&BusSubscriber]
    pending : Array[BusEvent]
    dispatching : Bool
    }

    EventBus: a product-level, cross-extension pub/sub channel.

    Posoco's core observer stream is core-owned by design (event emission is deliberately NOT exposed through CompositionView), so extensions that want to notify peers — a status bar learning a sprint's progress, a cache reporting its hit rate — need a channel of their own. This bus is that channel: the host (or a product layer such as cetas-core) constructs ONE bus and hands it to every extension constructor that wants to publish or subscribe.

    Contract:
    • Fire-and-forget: publishing with no subscribers is a no-op. An extension may publish unconditionally; when no peer cares, nothing happens and nothing leaks.
    • Topics are conventions, not registrations: every subscriber sees every event and ignores what it does not understand. topic is the shared vocabulary (e.g. "status" for status-line facts).
    • Synchronous and ordered: subscribers run in registration order, events in publish order. A publish from inside a handler is queued and dispatched after the in-flight batch (no unbounded recursion, no re-entrancy hazards).
    • Non-raising: a handler must not raise; the bus is a notification channel, never an error path.

    Example

    test "publish with no subscribers completes silently" {
    let bus = @devkit.EventBus::EventBus()
    bus.publish({
    source: "posoco_ext_scrum",
    topic: "status",
    data: Json::null(),
    })
    inspect(bus.subscriber_count(), content="0")
    }

    EventBus::EventBus

    fn EventBus::EventBus() -> EventBus

    Construct an empty bus. Typically done once per host process.

    EventBus::publish

    fn EventBus::publish(self : EventBus, event : BusEvent) -> Unit

    Notify every subscriber, in registration order. Reentrant publishes (issued from inside a handler) are queued and dispatched after the in-flight batch completes. Each batch is delivered to a snapshot of the subscriber list taken when the batch starts, so a registration made during a batch starts receiving with the next batch — never mid-batch.

    EventBus::subscribe

    fn EventBus::subscribe(self : EventBus, subscriber : &BusSubscriber) -> Unit

    Register a subscriber. Registration during an in-flight dispatch takes effect from the next batch onward.

    EventBus::subscriber_count

    fn EventBus::subscriber_count(self : EventBus) -> Int

    Number of live subscribers (observable for tests and diagnostics).

    ExtContext

    pub(all) struct ExtContext {
    logger : &Logger
    }

    ExtContext::ExtContext

    fn ExtContext::ExtContext(logger? : &Logger) -> ExtContext

    ExtContext::debug

    fn ExtContext::debug(self : ExtContext, source~ : String, code~ : String, message~ : String, data? : Json?) -> Unit

    ExtContext::default

    fn ExtContext::default() -> ExtContext

    ExtContext::error

    fn ExtContext::error(self : ExtContext, source~ : String, code~ : String, message~ : String, data? : Json?) -> Unit

    ExtContext::info

    fn ExtContext::info(self : ExtContext, source~ : String, code~ : String, message~ : String, data? : Json?) -> Unit

    ExtContext::log

    fn ExtContext::log(self : ExtContext, event : LogEvent) -> Unit

    ExtContext::warn

    fn ExtContext::warn(self : ExtContext, source~ : String, code~ : String, message~ : String, data? : Json?) -> Unit

    FileStamp

    pub(all) struct FileStamp {
    mtime_s : Int64
    mtime_ns : Int
    size : Int64
    } derive(Eq,
    Debug
    )

    Identity of a file at the moment it was read.

    The read/write/edit tools record a stamp at read time and compare it against a fresh stat of the same path before modifying it, so a write never silently clobbers content that changed after the model last saw it. mtime_s/mtime_ns are the (seconds, nanoseconds) pair surfaced by @fs.mtime on native and by statSync on js; size is the file length in bytes.

    FileStamp::equal

    fn FileStamp::equal(FileStamp, FileStamp) -> Bool

    FileStamp::not_equal

    fn FileStamp::not_equal(x : FileStamp, y : FileStamp) -> Bool

    FreshnessGuard

    pub struct FreshnessGuard {
    records : Map[String, FileStamp]
    }

    Read-before-modify ledger shared by the read/write/edit tool extensions.

    One guard instance per composed Agent: the host creates it and injects it into all three tools, so a read in one tool is visible to write/edit in the others. The guard itself touches no IO — callers stat files and pass stamps in. Its granularity is the Agent lifetime, matching a permission gate's session cache.

    FreshnessGuard::FreshnessGuard

    fn FreshnessGuard::FreshnessGuard() -> FreshnessGuard

    FreshnessGuard::check

    fn FreshnessGuard::check(self : FreshnessGuard, path : String, current : FileStamp?) -> FreshnessVerdict

    Classify a modification target against the guard.

    current is a stat of the file as it exists now. None means the path does not exist, in which case creating it is always allowed — a new file cannot have been clobbered.

    FreshnessGuard::forget

    fn FreshnessGuard::forget(self : FreshnessGuard, path : String) -> Unit

    Drop any recorded read for path (e.g. after the file was rewritten).

    FreshnessGuard::note_read

    fn FreshnessGuard::note_read(self : FreshnessGuard, path : String, stamp : FileStamp) -> Unit

    Record that path was successfully read with identity stamp.

    FreshnessVerdict

    pub(all) enum FreshnessVerdict {
    Fresh
    NeverRead
    Modified
    } derive(Eq,
    Debug
    )

    Outcome of checking a modification target against a FreshnessGuard.

    FreshnessVerdict::equal

    FreshnessVerdict::hint

    #as_free_fn(freshness_hint, deprecated="use `FreshnessVerdict::hint` instead", visibility="pub")
    fn FreshnessVerdict::hint(self : FreshnessVerdict) -> String

    Plain-language hint for a non-fresh verdict, for tool error messages.

    FreshnessVerdict::not_equal

    fn FreshnessVerdict::not_equal(x : FreshnessVerdict, y : FreshnessVerdict) -> Bool

    LogEvent

    pub(all) struct LogEvent {
    level : LogLevel
    source : String
    code : String
    message : String
    data : Json?
    } derive(
    Debug
    )

    LogEvent::to_repr

    LogLevel

    pub(all) enum LogLevel {
    Debug
    Info
    Warn
    Error
    } derive(Eq,
    Debug
    )

    LogLevel::equal

    fn LogLevel::equal(LogLevel, LogLevel) -> Bool

    LogLevel::not_equal

    fn LogLevel::not_equal(x : LogLevel, y : LogLevel) -> Bool

    LogLevel::to_repr

    MemoryLogger

    pub(all) struct MemoryLogger {
    events : Array[LogEvent]
    }

    MemoryLogger::MemoryLogger

    fn MemoryLogger::MemoryLogger() -> MemoryLogger

    MemoryLogger::log

    fn MemoryLogger::log(self : MemoryLogger, event : LogEvent) -> Unit

    MetricsPoint

    pub(all) struct MetricsPoint {
    model : String
    effort : String
    iq : Double
    cost_usd : Double
    passed : Int
    total : Int
    tokens : Double?
    minutes : Double?
    } derive(Eq,
    Debug
    )

    One vendor-stated benchmark point as published by an external metrics source: a single model+effort pair together with the source's iq (IQ-style score), cost_usd (the source's average cost per run in USD) and passed/total problem counts. Token totals and durations are what a consumer needs to compare completions of the same task across model+effort pairs; both are omitted when the source omits them — never zero-filled or estimated. Nothing here is estimated locally. The shape models codex-reset-radar's intelligence-efficiency-metrics entries (model/effort/iq/average_price_usd/passed/total plus the optional average_total_tokens/average_minutes).

    MetricsPoint::equal

    MetricsPoint::not_equal

    fn MetricsPoint::not_equal(x : MetricsPoint, y : MetricsPoint) -> Bool

    NoopLogger

    pub(all) struct NoopLogger {
    }

    NoopLogger::log

    fn NoopLogger::log(_self : NoopLogger, _event : LogEvent) -> Unit

    PricingPhase

    pub(all) struct PricingPhase {
    tier : String
    multiplier : String
    window : String
    } derive(Eq,
    Debug
    )

    One authoritative pricing phase as stated by a provider extension. tier is "peak" or "off-peak"; multiplier is the provider's own multiplier text (e.g. "2x"); window is the provider's own display text naming the surcharge window (e.g. "9:00 ~ 12:00, 14:00 ~ 18:00"). Nothing here is estimated locally: a PricingSource states phases, devkit never derives them.

    PricingPhase::equal

    PricingPhase::not_equal

    fn PricingPhase::not_equal(x : PricingPhase, y : PricingPhase) -> Bool

    QuotaReading

    pub(all) struct QuotaReading {
    window : String
    used_percent : Double?
    amount : (String, String)?
    available : Bool?
    reset_at_ms : Int64?
    fetched_at_ms : Int64
    } derive(Eq,
    Debug
    )

    One authoritative quota reading as stated by a provider. Nothing here is estimated locally: used_percent exists only when the provider stated an official percentage, and reset_at_ms only when the provider stated a reset time (balance-style providers have neither).

    window is a provider-owned opaque label string (examples seen: "5h", "weekly", "balance"); devkit defines no label vocabulary and never interprets labels.

    amount carries (value, currency) for balance-style readings whose semantics are "remaining funds", not "fraction of a window used". available is the provider's own yes/no on whether the account can currently serve requests, when it states one.

    QuotaReading::equal

    QuotaReading::not_equal

    fn QuotaReading::not_equal(x : QuotaReading, y : QuotaReading) -> Bool

    StatusOp

    pub(all) enum StatusOp {
    Register(segment~ : String, label~ : String?, priority~ : Int, value~ : String?, color~ : String?)
    Update(segment~ : String, value~ : String)
    Unregister(segment~ : String)
    } derive(Eq,
    Debug
    )

    One decoded status-bar operation.

    StatusOp::equal

    fn StatusOp::equal(StatusOp, StatusOp) -> Bool

    StatusOp::not_equal

    fn StatusOp::not_equal(x : StatusOp, y : StatusOp) -> Bool

    StatusOp::to_repr

    WorkspaceAnchor

    pub struct WorkspaceAnchor {
    root : String
    } derive(
    Debug
    )

    Workspace anchor: the directory that relative paths in tool calls resolve against. One anchor is built per host from the launch directory and threaded into every tool; the absolute root never appears in model-visible prompts.

    WorkspaceAnchor::WorkspaceAnchor

    fn WorkspaceAnchor::WorkspaceAnchor(root : String) -> WorkspaceAnchor

    Build an anchor. "" normalizes to "."; a trailing / is dropped (except on / itself) so joined paths never contain // at the seam.

    @devkit.WorkspaceAnchor::WorkspaceAnchor("/tmp/proj/").root,
    test { inspect( WorkspaceAnchor::WorkspaceAnchor("/tmp/proj/").root, content="/tmp/proj", ) }

    WorkspaceAnchor::resolve

    fn WorkspaceAnchor::resolve(self : WorkspaceAnchor, path : String) -> String

    Resolve path against the anchor. Absolute paths pass through unchanged; relative paths are joined onto the root and lexically normalized (. and .. collapse, // deduplicates). .. may rise above the root — containment is a permission concern, not an anchoring one. let anchor = @devkit.WorkspaceAnchor::WorkspaceAnchor("/tmp/proj")
    test {
    let anchor = @devkit.WorkspaceAnchor::WorkspaceAnchor("/tmp/proj")
    inspect(anchor.resolve("src/a/../b"), content="/tmp/proj/src/b")
    inspect(anchor.resolve("../sibling"), content="/tmp/sibling")
    }

    STATUS_TOPIC

    let STATUS_TOPIC : String

    The bus topic every status-bridge event travels on.

    context_envelope

    #alias(render_context_envelope, deprecated="renamed to context_envelope")
    fn context_envelope(ns~ : String, kind~ : String, trust~ : Bool, source? : String, attrs? : Array[(String, String)], content~ : String) -> String

    Render one context envelope element: <{ns}-context type="{kind}" trust="{trust}"[ source="{source}"][ {name}="{value}"…]…</{ns}-context>. ns is the composing host's bare namespace (e.g. cetas hosts pass "cetas" and get <cetas-context>); it, kind, and custom attribute names are trusted host configuration and are not escaped. source (rendered only when passed), custom attribute values, and content are escaped because they may carry external or dynamic text. Custom attributes render after the standard ones, in the given order.

    decode_status_op

    fn decode_status_op(event : BusEvent) -> StatusOp?

    Decode a bus event into a StatusOp. Returns None when the event is not on the status topic, the payload is not an object, op is missing or unknown, or a required field is missing or wrongly typed — callers drop the event and move on. priority is the one forgiving field: absent or non-numeric decodes to 0. Never raises.

    default_ignore_patterns

    fn default_ignore_patterns() -> Array[String]

    Directory and file names the file tools (grep, glob) prune during traversal. Covers VCS internals and the common build/vendor directories; dot-prefixed entries are additionally caught by has_hidden_segment. Hosts override per tool via each tool's constructor.

    glob_match

    fn glob_match(pattern : String, path : String) -> Bool

    Segment glob match: * matches any characters within one path segment, ** matches zero or more whole segments (a trailing ** matches everything remaining). Used for grep's glob file filter and ignore handling; richer syntax (?, {a,b}, classes) is not supported.

    has_hidden_segment

    fn has_hidden_segment(path : String) -> Bool

    Does any segment of path start with .? Mirrors ripgrep's default hidden-entry skipping; callers apply it below the scan base only, so an explicitly requested base like .config/x still searches.

    home_dir

    fn home_dir() -> String?

    Resolve the current process's user home directory; None when the environment provides none. The platform comes from @path.sep, which moonbitlang/x/path derives per target (C stub natively, process.platform on js).

    home_dir_from_env

    fn home_dir_from_env(windows~ : Bool, home~ : String?, userprofile~ : String?, homedrive~ : String?, homepath~ : String?) -> String?

    Resolution core over injected env values, so the platform matrix is testable anywhere without mutating process env.

    is_status_color_role

    fn is_status_color_role(role : String) -> Bool

    Whether role belongs to status_color_roles(). The protocol carries any declared string; vocabulary enforcement is the consumer's (bridge's) job.

    matches_ignore

    fn matches_ignore(path : String, patterns : Array[String]) -> Bool

    Does path match any ignore pattern? Same semantics as @workspace.matches_ignore (kept dependency-free here): the path equals the pattern, starts with pattern + "/", or any path segment equals the pattern. Patterns are plain names, no globs.

    metrics_registry_lookup

    fn metrics_registry_lookup(id : String) -> &ModelMetricsSource?

    The source registered under id, or None when nothing is registered.

    metrics_registry_register

    fn metrics_registry_register(id : String, source : &ModelMetricsSource) -> Unit

    Register source under id, replacing any previous registration for that id (last write wins).

    metrics_registry_unregister

    fn metrics_registry_unregister(id : String) -> Unit

    Drop the registration under id; a no-op for unknown ids.

    pricing_registry_lookup

    fn pricing_registry_lookup(id : String) -> &PricingSource?

    The source registered under id, or None when nothing is registered.

    pricing_registry_register

    fn pricing_registry_register(id : String, source : &PricingSource) -> Unit

    Register source under id, replacing any previous registration for that id (last write wins).

    pricing_registry_unregister

    fn pricing_registry_unregister(id : String) -> Unit

    Drop the registration under id; a no-op for unknown ids.

    publish_status_register

    fn publish_status_register(bus : EventBus, source~ : String, segment~ : String, label? : String, priority? : Int, value? : String, color? : String) -> Unit

    Publish a register op: claim segment with display metadata. priority orders segments on the bar (ascending, left to right). label, value and color keys are emitted only when present; priority is always emitted. color is a semantic role from status_color_roles().

    publish_status_unregister

    fn publish_status_unregister(bus : EventBus, source~ : String, segment~ : String) -> Unit

    Publish an unregister op: release the segment.

    publish_status_update

    fn publish_status_update(bus : EventBus, source~ : String, segment~ : String, value~ : String) -> Unit

    Publish an update op: set the segment's value. For a segment the bridge has not seen, this acts as an implicit register with default priority.

    quota_registry_lookup

    fn quota_registry_lookup(id : String) -> &QuotaSource?

    The source registered under id, or None when nothing is registered.

    quota_registry_register

    fn quota_registry_register(id : String, source : &QuotaSource) -> Unit

    Register source under id, replacing any previous registration for that id (last write wins).

    quota_registry_unregister

    fn quota_registry_unregister(id : String) -> Unit

    Drop the registration under id; a no-op for unknown ids.

    relative_to_base

    fn relative_to_base(base : String, sub : String) -> String

    Path of sub relative to base: drops a base + "/" prefix (or ./ when the base is .). Returns sub unchanged when it does not live under base lexically; returns "" when the two are equal.

    sanitize_label

    fn sanitize_label(label : String, limit? : Int) -> String

    Same as sanitize_path with a shorter default limit (120) suitable for diagnostic labels such as tool/server names in MCP.

    sanitize_path

    fn sanitize_path(path : String, limit? : Int) -> String

    Keep user-controlled paths bounded and single-line in diagnostics. Truncates at limit chars (default 160), appending ... when truncated. Every control character (newlines, tabs, escape sequences, DEL, …) is replaced with a space so the result cannot forge terminal output.

    status_color_roles

    fn status_color_roles() -> Array[String]

    The closed vocabulary of semantic color roles a register may declare. Roles are semantic markers aligned with host theme vocabulary (e.g. the cetas-js theme role names); raw color values (ANSI/hex) never travel on the protocol, and hosts may ignore any declaration. Returns a fresh array per call (default_ignore_patterns style): uppercase globals must be const, and const rejects arrays.

    strip_dot_prefix

    fn strip_dot_prefix(path : String) -> String

    Strip a leading ./ (ripgrep spells relative results this way; the walkers do not) so both engines emit identical paths.

    truncate_chars

    fn truncate_chars(s : String, cap : Int) -> String

    Truncate s to at most cap characters, cutting at a char boundary. Empty strings and short strings pass through unchanged.