Mesh Search¶
paglets search demonstrates a practical mobile-agent use case: move the
search logic to each host, scan local files there, and send only hits back to
the caller. This avoids repeatedly pulling remote file contents to one machine
and gives the caller incremental results while hosts are still scanning.
The core agent is:
It is a pure mobile agent, not a resident service. The CLI creates one parent
MeshSearchAgent on the entry host. The parent clones child agents to target
hosts, children search local paths, and search events travel back to the parent.
The public parent protocol uses typed operations; MeshFanoutMixin handles the
clone bookkeeping and CursorDrainMixin handles event cursors.
The command combines common ripgrep content search and fd filename search
features:
uv run paglets search grep TODO .
uv run paglets search grep -C 2 -t py "class .*Agent" src tests
uv run paglets search grep -i --hidden -g "*.md" paglets docs
uv run paglets search find README .
uv run paglets search find report --extension md --kind file
uv run paglets search grep --jsonl TODO .
By default the CLI dynamically discovers a reachable entry host, then the
parent search paglet clones to all online same-version mesh hosts, including
the entry host. Restrict the search to specific hosts with repeated --host
flags:
Paths are interpreted locally by each target host process. Searching . means
the current working directory of each host, not the directory where the CLI is
running.
Agent API¶
The search example exports the agent, state, request, event, and summary dataclasses:
from paglets.examples.search import (
HostSearchSummary,
MeshSearchAgent,
MeshSearchState,
SearchEvent,
SearchRequest,
)
SearchRequest is the serializable command surface. It covers two modes:
| Mode | Purpose |
|---|---|
grep |
Search file contents and emit match, context, count, or file events. |
find |
Search file, directory, or symlink names and emit file events. |
SearchEvent is the streamed event type. Match events include host, path, line
number, column, full line text, matching text, and match offsets for optional
highlighting. Context events carry neighboring lines. File and count events are
used for filename search, -l, --files-without-match, and -c.
HostSearchSummary records per-host totals such as scanned files, matched
files, match counts, path counts, errors, truncation, and duration. The parent
agent's final summary groups these host summaries under results and records
host-level failures under errors.
Streaming Flow¶
The search agent uses message delivery rather than polling a remote filesystem:
- The CLI creates one parent
MeshSearchAgenton the entry host. - The parent discovers target hosts and clones child agents to them.
- Each child traverses local paths with Python APIs and sends hit batches back to the parent as paglet messages.
- The parent appends events to state and wakes any
draincall waiting for new events. - The CLI long-polls
drain, printing events as soon as they arrive.
Inside a host, incoming paglet messages already invoke handle_message()
through the mailbox. There is no need for a paglet to busy-poll its mailbox.
The search parent uses wait_state() so a drain request sleeps efficiently
until a child message adds more events, all hosts finish, or the timeout elapses.
This start/drain shape is important because active paglets process one message
at a time in their child process. A parent message handler should not block
waiting for child result messages that must be delivered to that same parent.
The parent exposes these typed operations:
| Operation | Purpose |
|---|---|
start |
Store the SearchRequest, resolve targets, clone children, and return target metadata. |
child_events |
Append streamed hit events from a child and wake waiting drain calls. |
child_done |
Record the child host summary or host error and mark the host complete. |
drain |
Long-poll for events after a cursor, returning immediately when new events arrive. |
summary |
Return current aggregate state. |
cleanup |
Dispose child agents after the CLI has finished. |
Flow Diagrams¶
The below Mermaid sequenceDiagram explains the calling sequence
between the CLI, entry host, parent search paglet, child paglets, and local
filesystems:
sequenceDiagram
participant CLI as "paglets search CLI"
participant Entry as "entry host"
participant Parent as "parent MeshSearchAgent"
participant Child as "child MeshSearchAgent"
participant FS as "local filesystem"
CLI->>Entry: create parent agent
CLI->>Parent: start(SearchRequest, targets, timeout)
Parent->>Entry: available_hosts()
loop "for each target host"
Parent->>Child: clone_to(host)
Child->>FS: traverse and search local paths
Child-->>Parent: child_events(SearchEvent batch)
end
loop "until all hosts finish"
CLI->>Parent: drain(after_cursor, wait_timeout)
Parent-->>CLI: events, cursor, done, summary
end
Child-->>Parent: child_done(HostSearchSummary)
CLI->>Parent: cleanup()
The following Mermaid flowchart explains the parent and child program
flow without focusing on every individual call:
flowchart TD
Start["CLI starts search"] --> Parent["Create parent MeshSearchAgent"]
Parent --> Resolve["Resolve online same-version target hosts"]
Resolve --> Clone{"Any targets?"}
Clone -- "yes" --> Children["Clone child agents to targets"]
Clone -- "no" --> NoTargets["Record mesh error"]
Children --> LocalSearch["Each child searches local paths"]
LocalSearch --> EventBatch["Send child_events batches to parent"]
EventBatch --> Drain["CLI long-polls drain"]
Drain --> Print["Print streamed text or JSONL events"]
LocalSearch --> Done["Send child_done summary"]
Done --> Complete{"All hosts complete?"}
Complete -- "no" --> Drain
Complete -- "yes" --> Summary["Return final summary"]
NoTargets --> Summary
Summary --> Cleanup["Dispose child agents and parent"]
Search Options¶
Useful content-search options:
| Option | Meaning |
|---|---|
-i, -S |
Ignore case, or use smart case. |
-F |
Treat the pattern as a literal string. |
-w |
Match whole words. |
-A, -B, -C |
Include context lines around matches. |
-o |
Print only matching text. |
-c |
Print matching-line counts per file. |
-l, --files-without-match |
Print only matching or non-matching file paths. |
-g |
Include or exclude glob patterns; prefix with ! to exclude. |
-t, -T |
Include or exclude supported file types. |
--hidden, --no-ignore, --follow |
Control hidden paths, ignore files, and symlinks. |
--max-depth, --max-file-size, --max-results-per-host |
Bound local work. |
Useful filename-search options:
| Option | Meaning |
|---|---|
--full-path |
Match against the full path instead of only the basename. |
-e, --extension |
Limit results to one extension; repeatable. |
--kind |
Emit only file, dir, symlink, or any paths. |
The implementation is intentionally a practical subset, not a byte-for-byte
clone of ripgrep or fd. Use paglets search --help,
paglets search grep --help, paglets search find --help, and
paglets search --type-list for the supported command surface.
Programmatic Use¶
Most users should use paglets search, but other paglets can create the search
agent directly and drain streamed events:
from paglets.examples.search import (
SEARCH_CLEANUP,
SEARCH_DRAIN,
SEARCH_START,
MeshSearchAgent,
SearchDrainRequest,
SearchRequest,
SearchStartRequest,
)
from paglets.patterns.operations import OperationClient
from paglets.serialization.codec import dataclass_to_wire
proxy = self.context.create_paglet(MeshSearchAgent)
client = OperationClient(proxy)
client.call(
SEARCH_START,
SearchStartRequest(
request=dataclass_to_wire(SearchRequest(mode="grep", pattern="TODO", paths=["."])),
timeout=60.0,
)
)
cursor = 0
while True:
reply = client.call(SEARCH_DRAIN, SearchDrainRequest(after_cursor=cursor, wait_timeout=0.5, limit=100))
for event in reply.events:
cursor = max(cursor, int(event["cursor"]))
# Render or forward the event here.
if reply.done:
break
summary = reply.summary
client.call(SEARCH_CLEANUP)
proxy.dispose()