Insight
induscode.insightis the agent's observability plane — a thin app-vocabulary wrapper over theindusagi.tracingframework (Probe/Trail/Signal aliased onto the framework's Segment/trace/TraceSignal) plus a ported in-memory collector sink and NDJSON replay readers. Reach it withfrom induscode.insight import create_recorder, ....
insight/ is the single observability subsystem for the coding agent. By a locked design decision it is a wrapper, not a re-port of the framework's tracing machinery: it renames the framework vocabulary into the app's, pins app-specific constants on top of indusagi.tracing, and adds the two pieces the framework lacks — an in-memory CollectorSink and NDJSON replay_* readers over the framework's own on-disk record schema. It is the lower-level distributed-tracing plane: timed probes, sampling, redaction, sinks, and replay.
Table of Contents
- What This Is Not
- Module Layout
- The Recorder
- Probes, Handles, and Faults
- Signals
- Sampling Gates
- Secret Redaction
- Sinks and the Collector
- Replay
- Public Exports
- Notable Behaviour
- The kit Utilities
What This Is Not
A common framing is that this package surfaces "usage / cost / token / context stats in the footer." It does not. There is no cost, token, or usage accounting code anywhere in insight/. Those footer statistics are rendered by the framework's Footer / SessionSnapshot widgets, surfaced through console/components/status_bar.py. That footer and this package are conceptually adjacent but have no code dependency between them.
insight/ is the distributed-tracing plane: it times spans of work (probes), decides which trails to sample, scrubs secrets out of attributes, fans signals to sinks, and reads persisted traces back from disk.
Module Layout
Three modules behind one barrel (__init__.py):
| Module | Role |
|---|---|
insight/wrapper.py |
The app vocabulary aliased onto the framework — InsightRecorder, Probe, ProbeHandle, the Signal union, sampling gates, the SecretRedactor, and the locked kind map. |
insight/collector.py |
The in-memory CollectorSink the framework lacks (ported), capturing every signal phase in arrival order. |
insight/replay.py |
NDJSON readers over the framework's record schema — replay_signals / replay_probes / replay_trails plus their lower-level helpers. |
The framework transport and terminal sinks (SignalChannel, Sink, ConsoleSink, FileSink, StreamSink, SecretPattern) are re-exported unchanged through the barrel, so consumers import the whole insight surface from induscode.insight rather than reaching into indusagi.tracing.
The locked kind map renames the framework's segment kinds into the app vocabulary:
App ProbeKind |
Framework segment kind |
|---|---|
run |
run |
turn |
inference |
tool |
action |
model |
recall |
custom |
custom |
These are pinned in KIND_TO_SEGMENT / SEGMENT_TO_KIND; PROBE_KINDS is the closed set and ID_WIDTHS holds the id byte widths.
The Recorder
InsightRecorder is the single facade the agent talks to. Build one with create_recorder(...); it owns a framework SignalChannel, the admission gate, the redactor, an injectable clock, and per-sink fan-out drain tasks.
from induscode.insight import create_recorder, ratio_gate, create_collector_sink
async def trace_a_run():
sink = create_collector_sink()
recorder = create_recorder(
service="induscode",
gate=ratio_gate(0.1), # sample 10% of trails, keyed on trail id
sinks=(sink,), # requires a running event loop
)
root = recorder.open_trail("session", kind="run")
step = root.child("tool", "read_file", {"path": "/etc/hosts"})
step.note({"bytes": 1234})
step.close("ok")
root.close("ok")
await recorder.shutdown() # flush, close channels, await drains
return sink.probes()
create_recorder keyword arguments:
| Kwarg | Default | Purpose |
|---|---|---|
service |
"insight" |
Service label for the channel. |
enabled |
True |
Master switch; when False, open_trail always returns the no-op handle. |
gate |
always_gate |
Admission gate (always_gate / never_gate / ratio_gate(f)). |
redactor |
PASSTHRU_REDACTOR |
Attribute processor applied on input. |
sinks |
() |
Framework Sink instances; each gets its own fan-out channel and drain task. |
now |
wall-clock | Injectable () -> int clock for deterministic tests. |
channel |
fresh | Override the framework SignalChannel. |
Recorder methods:
| Method | Purpose |
|---|---|
open_trail(name, *, kind="run", trail_id=None, attributes=None) |
Open a brand-new trail; returns its root ProbeHandle, or the shared NOOP_HANDLE when disabled or gated out. |
scope(trail_id) |
Bind a frozen TrailRecorder view to an existing trail id. |
signals() |
Single-consumer async adapter that recovers app-vocabulary Signal values from the framework channel. |
flush() |
Yield a tick so synchronously-enqueued signals settle in sink tasks. |
shutdown() |
Flush, close every channel, and await the sink drain tasks. |
TrailRecorder is a frozen recorder view bound to one trail id (the framework Recorder.scope surface). It exposes open(kind, name, attrs) and child(parent_id, kind, name, attrs), both returning NOOP_HANDLE when the trail was not sampled.
Probes, Handles, and Faults
A Probe is an immutable timed segment within a trail — the app view of a framework Segment. Fields: id, trail_id, parent_id, kind, name, started_at, ended_at, status, attributes, fault. An ended_at of None means the probe is still in-flight.
A ProbeHandle is the behavioural face of one in-flight probe (a protocol):
| Method | Behaviour |
|---|---|
note(attributes) |
Attach attributes (scrubbed on input); no-op after close. |
child(kind, name, attrs) |
Open a child probe; returns its handle. On NOOP_HANDLE this returns itself, so a sampled-out subtree costs nothing. |
fail(error) |
Mark the probe failed, carrying rich fault info; no-op after close. |
close(outcome=None) |
Terminate the probe; idempotent. |
A ProbeFault is the captured failure attached to a probe — message plus optional name / stack (richer than the framework's message-only SegmentError). fault_of(value) coerces any raised value into a serializable ProbeFault: an exception becomes message + type name + rendered traceback, anything else becomes str(value).
Two projection helpers bridge the two vocabularies:
probe_from_segment(segment)projects a frameworkSegmentonto aProbe, mapping the kind back to app vocabulary and lifting the reserved faultname/stackattributes ontoProbe.fault.is_closed_probe(probe)isTruewhen a probe reached a terminal state (status != openandended_at is not None).
Signals
On the wire, each probe transition is a Signal. The app-vocabulary union has three members, each carrying (probe, at) with a ClassVar phase literal:
Signal = OpenSignal | UpdateSignal | CloseSignal
recorder.signals() is the single-consumer async iterator that recovers these from the framework channel. Because the framework's update signal carries only (id, attributes), UpdateSignal probes are reconstructed statefully from the open they amend, and the update's at is stamped at observation time. is_close_signal(signal) narrows a Signal to a terminal CloseSignal.
fail() is brought to parity by emitting an in-flight UpdateSignal whose reserved attributes (insight.fault.message / .name / .stack) carry the failure past the framework's status-less update; the adapter lifts them back into an error-status probe and strips the reserved keys so they never surface as caller attributes.
Sampling Gates
Gates decide whether a trail is admitted; they delegate to the framework SampleGate / RatioStrategy. All gates expose verdict(trail_id) -> bool.
| Gate | Behaviour |
|---|---|
always_gate |
Admit everything (shared _FixedGate instance; recorder default). |
never_gate |
Reject everything (shared _FixedGate instance). |
ratio_gate(fraction) / RatioGate |
Admit a deterministic fraction keyed on the trail id. |
RatioGate keys its decision on an FNV-1a hash of the trail id that is documented-verified bit-identical to the TS hash32 for fixture ids (a*32 → 1297108005, b*32 → 2615884229), which is exactly why the framework gate is reused rather than re-ported. A NaN fraction fails closed to 0.
Secret Redaction
SecretRedactor is the attribute processor applied on input (open / child / note / fail). Build one with create_redactor(...); DEFAULT_REDACTOR is the configured scrubber and PASSTHRU_REDACTOR (no redaction) is the recorder's default.
The redactor diverges from the framework SecretScrubber in a single walk over the attribute subtree:
- key rules tokenize a whole subtree,
- value rules
re.subonly the matched run (the framework scrubber replaces the whole value), omit_keysdrops keys entirely,- a 4096-char length cap is applied in the same pass.
The app constants behind it: APP_SECRET_PATTERNS (one key rule plus per-credential-shape value rules, built on the framework SecretPattern), REDACTED_TOKEN ("[insight:scrubbed]"), DEFAULT_MAX_STRING_LENGTH (4096), and TRUNCATION_SUFFIX ("...[insight:truncated]").
Fresh ids are minted via mint_trail_id() (128-bit / 32-hex) and mint_probe_id() (64-bit / 16-hex), wrapping the framework's fresh_trace_id / fresh_segment_id.
Sinks and the Collector
The framework ships console / file / stream sinks but no in-memory collector, so insight/ ports one. CollectorSink (via create_collector_sink(id="collector")) is a regular framework Sink that drains a SignalChannel and appends every signal — open, update, and close — in arrival order, unlike the file and stream sinks which persist closes only.
sink = create_collector_sink()
recorder = create_recorder(sinks=(sink,))
# ... drive the recorder ...
sink.signals # every raw framework signal, in arrival order
sink.close_records # terminal TraceRecords carried by close signals
sink.probes() # app-vocabulary Probe views of the close records
The framework FileSink / StreamSink write only close records as flat camelCase NDJSON (id / traceId / parentId / kind / name / startedAt / endedAt / status / attributes / error).
Replay
replay.py reads the framework's NDJSON record schema back into the app vocabulary. The high-level readers are async:
| Reader | Yields |
|---|---|
replay_signals(source, *, strict=False) |
A stream of Signals (phase derived per record — terminal → close @endedAt, else open @startedAt). |
replay_probes(source, *, strict=False) |
A collapsed last-write-wins Probe dict keyed by id. |
replay_trails(source, *, strict=False) |
Per-trail ReplayedTrail parent→children trees with a completeness flag. |
Malformed lines are skipped unless strict=True. The lower-level helpers compose into the readers:
read_lines(source)— UTF-8 incremental, multi-byte-safe chunk → line splitter.read_records(source, *, strict=False)— validated record-dict stream.decode_record(line)— single-line JSON parse.record_to_probe(record)— record-dict →Probeprojection (lifts the reserved faultname/stack).
A ReplayedTrail is one reconstructed trail (trail_id, root, probes map, parent→children adjacency, complete flag). ChunkSource is the sync-or-async raw text/byte chunk source type alias.
Public Exports
| Name | Kind | Source | Purpose |
|---|---|---|---|
InsightRecorder / create_recorder |
class / function | wrapper.py |
The recorder facade and its factory. |
TrailRecorder |
dataclass | wrapper.py |
Frozen recorder view bound to one trail id. |
Probe / ProbeHandle / ProbeFault |
dataclass / class | wrapper.py |
The timed segment, its in-flight handle, and a captured failure. |
fault_of / probe_from_segment / is_closed_probe |
function | wrapper.py |
Fault coercion, framework→app projection, terminal-state predicate. |
OpenSignal / UpdateSignal / CloseSignal / Signal |
dataclass / type | wrapper.py |
The wire signal union. |
is_close_signal |
function | wrapper.py |
Narrow a Signal to a CloseSignal. |
always_gate / never_gate / ratio_gate / RatioGate |
const / class | wrapper.py |
Sampling gates. |
SecretRedactor / create_redactor / DEFAULT_REDACTOR / PASSTHRU_REDACTOR |
class / const | wrapper.py |
The attribute redactor and presets. |
mint_trail_id / mint_probe_id |
function | wrapper.py |
Mint fresh trail / probe ids. |
KIND_TO_SEGMENT / SEGMENT_TO_KIND / PROBE_KINDS / ID_WIDTHS |
const | wrapper.py |
The locked kind map, closed kind set, id widths. |
APP_SECRET_PATTERNS / REDACTED_TOKEN / DEFAULT_MAX_STRING_LENGTH / TRUNCATION_SUFFIX |
const | wrapper.py |
The app redaction rule set and constants. |
NOOP_HANDLE |
const | wrapper.py |
The shared frozen sampled-out handle. |
CollectorSink / create_collector_sink |
class / function | collector.py |
The in-memory all-phase sink. |
replay_signals / replay_probes / replay_trails |
async function | replay.py |
NDJSON record-schema readers. |
read_lines / read_records / decode_record / record_to_probe |
function | replay.py |
Lower-level replay helpers. |
ReplayedTrail / ChunkSource |
dataclass / type | replay.py |
A reconstructed trail and the chunk source alias. |
SignalChannel / Sink / ConsoleSink / FileSink / StreamSink / SecretPattern |
class | indusagi.tracing |
Framework transport and sinks re-exported unchanged. |
Notable Behaviour
- No cost / token / usage / footer accounting lives here — those stats come from the framework
Footer/SessionSnapshotwidgets in the console. This package is the tracing plane only. - Not yet wired. A grep finds no importers of
induscode.insightanywhere outside the package itself — it is built and self-consistent but currently unconsumed by the agent runtime. - Event-loop requirement. Passing
sinks=to the recorder callsasyncio.get_running_loop().create_task(...)in__init__, so constructing a recorder with sinks outside a running loop raises. - Single-consumer.
signals()andCollectorSink.drain()/ the framework channel are single-consumer — call once and drain. Multiple sinks each get their own fan-out channel. - Reserved fault keys.
insight.fault.{message,name,stack}are the channel that carries rich fault info past the framework's message-onlySegmentError; they are stripped from the exposed attribute bag inprobe_from_segment/record_to_probe, so they only surface onProbe.fault. - Scrubbing on input, not at emit, and update probes are reconstructed statefully with an observation-time
at— documented divergences the ported test suite does not need to observe. - Foreign-writer tolerance. Replay accepts a writer that persists open records (phase derived per record), even though framework sinks only write closes.
The kit Utilities
induscode.kit is the bottom-most leaf layer of the agent — a small set of framework-agnostic, stdlib-only helpers with zero imports of the indusagi framework and zero imports of any sibling subpackage. Higher layers compose it; it depends on nothing. Six files, ~982 lines. Consumers import from the barrel (from induscode.kit import ...) rather than reaching into modules. The only confirmed consumer today is the console: console/app.py imports open_in_external_editor and read_clipboard_image.
Five independent module groups:
| Module | Job |
|---|---|
kit/shell.py |
POSIX shell-argument quoting (quote_arg, needs_quoting, build_shell_command). |
kit/image.py |
PNG / JPEG magic-byte sniffing and release-asset-name rendering. |
kit/clipboard_image.py |
Pull a raster image off the OS clipboard, stage it as a temp PNG. |
kit/external_editor.py |
Hand the composer buffer off to $VISUAL / $EDITOR / vi. |
kit/tool_fetch.py |
A managed-binary (fd / rg) provisioner stub — resolution and URL-building only. |
Selected exports:
| Name | Kind | Source | Purpose |
|---|---|---|---|
quote_arg / needs_quoting / build_shell_command |
function | kit/shell.py |
POSIX-quote one arg, test whether quoting is needed, join an arg sequence into a shell line. |
sniff_image_format / detect_image_media_type / is_supported_image |
function | kit/image.py |
Classify a ByteSource as png / jpeg (reads at most IMAGE_SNIFF_BYTES = 8). |
resolve_asset_name / AssetNameContext |
function / dataclass | kit/image.py |
{platform} / {arch} / {version} / {name} token substitution for release assets. |
read_clipboard_image / ClipboardImageOptions |
function / dataclass | kit/clipboard_image.py |
Best-effort clipboard → temp PNG; None on any miss, hermetically testable. |
open_in_external_editor / ExternalEditorOptions |
function / dataclass | kit/external_editor.py |
Stage a buffer in a temp .md, launch the editor, return edited text or None. |
resolve_managed_binary_path |
function | kit/tool_fetch.py |
Pure path builder for a managed binary (bare on POSIX, +.exe on win32). |
pick_release_asset / build_download_request / plan_provision |
function | kit/tool_fetch.py |
Pick the host asset, render a DownloadRequest, assemble a ProvisionPlan (network injected as ReleaseLookup). |
ToolDescriptor / ReleaseAsset / ReleaseInfo / DownloadRequest / ProvisionPlan |
dataclass | kit/tool_fetch.py |
The provisioner value types (all frozen + slots). |
KIT_USER_AGENT |
const | kit/tool_fetch.py |
"indusagi-kit-provisioner/1.0" — the only literal mention of indusagi in kit. |
Design notes that matter:
tool_fetch.pyis a stub: it computes paths, picks assets, and builds requests but never downloads, unpacks, or installs anything — that is the caller's job. The network is a deliberately injectedReleaseLookupprotocol, so the module imports no HTTP client and stays offline-testable.- The clipboard tries
pngpastethen anosascript «class PNGf»fallback on darwin, andwl-paste(Wayland) thenxclipon linux. The I/O modules are best-effort: every spawn error, timeout, non-zero exit, oversized capture (>50 MiB), or empty result collapses toNone. - The barrel deliberately omits the upstream generic stdlib-clone "filler" libraries (array / string / date / json helpers) because they had zero consumers.
See Architecture for the subsystem map, and Package Exports for the full namespace catalog.
