Sources
Pure functions cannot read files, make HTTP requests, or listen for keyboard input. They take values and return values — that is their strength.
But a desktop application needs to talk to the outside world. Sources are how AIVI bridges that gap. A source is a typed, declared entry point that feeds external data into the reactive graph:
Outside world → @source → Signal → Pure derivations → UI
(keyboard, (typed (reactive (your functions) (GTK
HTTP, boundary) graph) widgets)
timers,
files)Inside the boundary, everything is deterministic. Outside, the runtime handles the mess.
For the current compiler-and-runtime-backed reference of every built-in source kind and option, see the Built-in Source Catalog.
Unified external boundary
Built-in capability handles are now the public external surface for the built-in families that have both reactive reads and one-shot commands. Modules such as aivi.fs, aivi.http, aivi.env, aivi.log, aivi.stdio, and aivi.random provide shared vocabulary for those handles. Some older task helpers, notably JSON-as-text helpers, still exist; the capability model is the preferred external-integration path, not a claim that all legacy effectful exports have been removed.
Current shape:
value projectRoot : Text = "/tmp/demo"
value configPath : Text = "config.json"
@source fs projectRoot
signal files : FsSource
signal config : Signal (Result FsError Text) = files.read configPath
value cleanup : Task Text Unit = files.deleteFile "cache.txt"In that model:
- reads, watches, queries, and subscriptions stay source/reactive
- mutations become explicit provider-owned commands on the same capability
- incoming data decodes directly into the annotated target type
- host snapshots such as environment/process/XDG data use the same provider boundary
- sink-style effects such as logging, stdio writes, D-Bus method calls, and outbound sends do too
- raw JSON-as-text helper workflows are not the public external design anymore
Built-in capability handles
Built-in provider families now support bodyless handle anchors plus direct top-level member use. The compiler lowers those forms onto the existing built-in source providers, task intrinsics, and pure host-context intrinsics:
value projectRoot : Text = "/tmp/demo"
@source fs projectRoot
signal files : FsSource
signal config : Signal (Result FsError Text) = files.read "config.json"
value cleanup = files.deleteFile "cache.txt"Today this lowering is implemented for fs, http, db, env, log, stdio, random, process, path, dbus, and tray.
Current rules:
- handle anchors must stay bodyless and use a nominal non-
Signalannotation such asFsSource - direct
signal name : Signal T = handle.member ...forms lower to ordinary bodyless source bindings with synthesized@source provider.variant ...metadata - direct
value name = handle.member ...forms lower through the built-in handle task path for commands, queries, and host snapshots - capability handles are compile-time anchors, not exported runtime signals
- custom provider contracts may declare
operationandcommandmembers already - direct
signal name : Signal T = handle.member ...lowering now works for custom provider operations too; those lower to member-qualified custom source bindings such as@source custom.feed.read ... - direct custom command handle values are still pending a generic task/runtime bridge
Source-backed signals with @source
Today, built-in sources are attached with the @source decorator immediately before the signal declaration:
@source timer.every 120 with {
immediate: True,
coalesce: True
}
signal tick : Signal Unit
value view =
<Window title="Timer">
<Label text="Timer source active" />
</Window>That defines tick as a timer-driven signal.
Window input
type Key = Key Text
@source window.keyDown with {
repeat: False,
focusOnly: True
}
signal keyDown : Signal Key
value view =
<Window title="Keys">
<Label text="Press a key" />
</Window>HTTP requests
type User = {
id: Int,
name: Text
}
@source http "https://api.example.com"
signal api : HttpSource
signal users : Signal (HttpResponse (List User)) = api.get "/users"
value view =
<Window title="Users">
<Label text="Loading users" />
</Window>File watching
type FsWatchEvent =
| Created
| Changed
| Deleted
@source fs.watch "/tmp/demo.txt" with {
events: [Created, Changed, Deleted]
}
signal fileEvents : Signal FsWatchEvent
value view =
<Window title="Watcher">
<Label text="Watching files" />
</Window>Spawning a process
type StreamMode =
| Ignore
| Lines
| Bytes
type ProcessEvent =
| Spawned
@source process.spawn "rg" ["TODO", "."] with {
stdout: Lines,
stderr: Ignore
}
signal grepEvents : Signal ProcessEvent
value view =
<Window title="Search">
<Label text="Running rg" />
</Window>Custom providers
You can also declare a provider contract. Argument and option declarations still describe the @source boundary itself; operation and command declarations preserve the capability-member surface in HIR so later custom-provider handle lowering can target one provider-owned API. Contract schemas currently accept primitives and types or domains declared in the same module, including those shapes under List or Signal:
type Mode =
| Stream
domain Timeout over Int = {
suffix ms : Int = value => Timeout value
}
provider custom.feed
argument path : Text
option timeout : Timeout
option mode : Mode
operation read : Text -> Signal Int
command delete : Text -> Task Text Unit
wakeup: providerTrigger
@source custom.feed "/tmp/demo.txt" with {
timeout: 5ms,
mode: Stream
}
signal updates : Signal Int
value view =
<Window title="Feed">
<Label text="Custom provider" />
</Window>Summary
| Form | Meaning |
|---|---|
@source timer.every ... | Timer-backed signal |
@source window.keyDown ... | Window input signal |
@source http.get ... | HTTP-backed signal |
@source fs.watch ... | File watch signal |
@source process.spawn ... | Process-backed signal |
provider custom.feed | Custom source/capability contract |
| Built-in Source Catalog | Current source-kind and option reference |