Skip to content

Commit 47c1cca

Browse files
committed
fix(docs): clarify quickstart + Mission Control Ch 1-2 from user feedback
60-second quickstart (python.mdx): - Lede now leads with the cross-machine promise - Define the terms "service" and "consumer" inline (not just "producer") - Python consumer uses dynamic proxy — drops the "from hello_service import" line that broke cross-machine demos - Windows PowerShell variants for ASTER_ENDPOINT_ADDR commands - New "What just happened" section explains the 3 steps + cross-network semantics - Drop "Dev mode vs production" — out of scope for 60 seconds - "What's next" reduced to single Mission Control CTA Mission Control walkthrough (mission-control.mdx): - New "Skip to the code" tip box at the top linking the Python and TypeScript example repos for readers who'd rather clone and run - Chapter 1: explicit "this is the address from control.py" framing for aster shell / aster call placeholders, ephemeral-key warning, Windows PowerShell variant using --% (the stop-parsing operator) - Chapter 2 fully rewritten: - Full control.py / control.ts files instead of "... from Chapter 1 ..." snippets, so readers know exactly what to copy - Explicit restart instructions + reminder that the address changes - New logs.py / logs.ts test submitter so tailLogs has data to stream - CLI command now includes the missing "cd services/MissionControl" - Three-terminals callout explaining the natural shape of a streaming demo (server, submitter, tail consumer)
1 parent 41134d9 commit 47c1cca

2 files changed

Lines changed: 275 additions & 104 deletions

File tree

docs/quickstart/mission-control.mdx

Lines changed: 243 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ import LanguageTabs, {TabItem} from '@site/src/components/LanguageTabs';
1616
>
1717
> Or: you write one file and run it.
1818
19+
:::tip Don't want to follow along? Skip to the code.
20+
The complete, working source for every chapter in this guide lives in the main repo. Clone, run, read.
21+
22+
- **Python:** [examples/python/mission_control](https://github.com/aster-rpc/aster-rpc/tree/main/examples/python/mission_control)
23+
- **TypeScript:** [examples/typescript/missionControl](https://github.com/aster-rpc/aster-rpc/tree/main/examples/typescript/missionControl)
24+
:::
25+
1926
<LanguageTabs>
2027
<TabItem value="python">
2128

@@ -197,10 +204,11 @@ if __name__ == "__main__":
197204
```bash
198205
# Start the control plane
199206
python control.py
200-
# → aster1Qm...
207+
# → aster1Qmxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
208+
# ^^^^^^^ this is your control plane's public-key address — copy it for the next step
201209
```
202210

203-
In another terminal, connect and inspect:
211+
In another terminal, connect and inspect. **Replace `aster1Qm...` below with the address your `control.py` just printed** (the address is unique to each run; it's an ephemeral key in dev mode):
204212

205213
```bash
206214
aster shell aster1Qm...
@@ -254,10 +262,11 @@ main();
254262
```bash
255263
# Start the control plane
256264
bun run control.ts
257-
# → aster1Qm...
265+
# → aster1Qmxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
266+
# ^^^^^^^ this is your control plane's public-key address — copy it for the next step
258267
```
259268

260-
In another terminal, connect and inspect:
269+
In another terminal, connect and inspect. **Replace `aster1Qm...` below with the address your `control.ts` just printed** (the address is unique to each run; it's an ephemeral key in dev mode):
261270

262271
```bash
263272
aster shell aster1Qm...
@@ -268,12 +277,20 @@ aster shell aster1Qm...
268277
</TabItem>
269278
</LanguageTabs>
270279

271-
Or skip the shell entirely — call it straight from the command line:
280+
Or skip the shell entirely — call it straight from the command line. Replace `aster1Qm...` with your address:
272281

273282
```bash
283+
# macOS / Linux
274284
aster call aster1Qm... MissionControl.getStatus '{"agent_id": "edge-node-7"}'
275285
```
276286

287+
```powershell
288+
# Windows (PowerShell). The --% operator tells PowerShell to stop parsing
289+
# and pass the rest of the line verbatim — necessary because PowerShell's
290+
# argument parser strips quotes from JSON arguments otherwise.
291+
aster call aster1Qm... MissionControl.getStatus --% {"agent_id": "edge-node-7"}
292+
```
293+
277294
> **`aster shell` vs `aster call`:** Use `aster shell` for interactive
278295
> exploration — browsing services, tab-completing methods, streaming.
279296
> Use `aster call` for scripting and one-shot invocations. Both use
@@ -295,18 +312,33 @@ aster call aster1Qm... MissionControl.getStatus '{"agent_id": "edge-node-7"}'
295312
**Goal:** Agents push logs into the control plane. Operators tail them
296313
in real time using server streaming.
297314

315+
This chapter extends `control.py` (or `control.ts`) with two new methods: `submitLog` (a unary RPC) and `tailLogs` (a server-streaming RPC). To save you from puzzling over which lines go where, the full updated file is below — replace your existing `control.py` / `control.ts` with it.
316+
298317
<LanguageTabs>
299318
<TabItem value="python">
300319

320+
Replace your `control.py` with this complete version:
321+
301322
```python
323+
# control.py
324+
import asyncio
302325
from collections.abc import AsyncIterator
303-
from aster import server_stream
326+
from dataclasses import dataclass
327+
from aster import AsterServer, service, rpc, server_stream, wire_type
304328

305-
# Ordered severity used by the level filter below.
306-
_LEVEL_ORDER = {"debug": 0, "info": 1, "warn": 2, "error": 3, "fatal": 4}
329+
# ─────────────── Wire types ───────────────
307330

308-
def _level_rank(level: str) -> int:
309-
return _LEVEL_ORDER.get(level.lower(), 0)
331+
@wire_type("mission/StatusRequest")
332+
@dataclass
333+
class StatusRequest:
334+
agent_id: str = ""
335+
336+
@wire_type("mission/StatusResponse")
337+
@dataclass
338+
class StatusResponse:
339+
agent_id: str = ""
340+
status: str = "idle"
341+
uptime_secs: int = 0
310342

311343
@wire_type("mission/LogEntry")
312344
@dataclass
@@ -325,19 +357,35 @@ class SubmitLogResult:
325357
@dataclass
326358
class TailRequest:
327359
agent_id: str = ""
328-
level: str = "info" # minimum level filter
360+
level: str = "info" # minimum level filter
361+
362+
# ─────────────── Helpers ───────────────
363+
364+
_LEVEL_ORDER = {"debug": 0, "info": 1, "warn": 2, "error": 3, "fatal": 4}
365+
366+
def _level_rank(level: str) -> int:
367+
return _LEVEL_ORDER.get(level.lower(), 0)
368+
369+
# ─────────────── Service ───────────────
329370

330371
@service(name="MissionControl", version=1)
331372
class MissionControl:
332373
def __init__(self):
333-
self._log_queue = asyncio.Queue()
374+
self._log_queue: asyncio.Queue[LogEntry] = asyncio.Queue()
334375

335-
# ... getStatus from Chapter 1 ...
376+
@rpc()
377+
async def getStatus(self, req: StatusRequest) -> StatusResponse:
378+
return StatusResponse(
379+
agent_id=req.agent_id,
380+
status="running",
381+
uptime_secs=3600,
382+
)
336383

337384
@rpc()
338385
async def submitLog(self, entry: LogEntry) -> SubmitLogResult:
339386
"""Agents call this to push log entries."""
340387
await self._log_queue.put(entry)
388+
return SubmitLogResult(accepted=True)
341389

342390
@server_stream()
343391
async def tailLogs(self, req: TailRequest) -> AsyncIterator[LogEntry]:
@@ -349,20 +397,52 @@ class MissionControl:
349397
if _level_rank(entry.level) < _level_rank(req.level):
350398
continue
351399
yield entry
400+
401+
# ─────────────── Main ───────────────
402+
403+
async def main():
404+
async with AsterServer(services=[MissionControl()]) as srv:
405+
print(srv.address) # compact aster1... address
406+
await srv.serve()
407+
408+
if __name__ == "__main__":
409+
asyncio.run(main())
410+
```
411+
412+
**Restart the service.** Stop the previous run with `Ctrl+C`, then start the new version:
413+
414+
```bash
415+
python control.py
416+
# → aster1Qmxxxxxxxx... ← this is a NEW address; the old one is gone
352417
```
353418

419+
The address changes on every restart in dev mode (it's an ephemeral key). Copy the new address — you'll need it for both terminals below.
420+
354421
</TabItem>
355422
<TabItem value="typescript">
356423

424+
Replace your `control.ts` with this complete version:
425+
357426
```typescript
358-
import { ServerStream } from '@aster-rpc/aster';
427+
// control.ts
428+
import {
429+
AsterServer, Service, Rpc, ServerStream, WireType,
430+
} from '@aster-rpc/aster';
359431

360-
// Ordered severity used by the level filter below.
361-
const LEVEL_ORDER: Record<string, number> = {
362-
debug: 0, info: 1, warn: 2, error: 3, fatal: 4,
363-
};
364-
function levelRank(level: string): number {
365-
return LEVEL_ORDER[level.toLowerCase()] ?? 0;
432+
// ─────────────── Wire types ───────────────
433+
434+
@WireType("mission/StatusRequest")
435+
class StatusRequest {
436+
agent_id: string = "";
437+
constructor(init?: Partial<StatusRequest>) { if (init) Object.assign(this, init); }
438+
}
439+
440+
@WireType("mission/StatusResponse")
441+
class StatusResponse {
442+
agent_id: string = "";
443+
status: string = "idle";
444+
uptime_secs: number = 0;
445+
constructor(init?: Partial<StatusResponse>) { if (init) Object.assign(this, init); }
366446
}
367447

368448
@WireType("mission/LogEntry")
@@ -387,12 +467,30 @@ class TailRequest {
387467
constructor(init?: Partial<TailRequest>) { if (init) Object.assign(this, init); }
388468
}
389469

470+
// ─────────────── Helpers ───────────────
471+
472+
const LEVEL_ORDER: Record<string, number> = {
473+
debug: 0, info: 1, warn: 2, error: 3, fatal: 4,
474+
};
475+
function levelRank(level: string): number {
476+
return LEVEL_ORDER[level.toLowerCase()] ?? 0;
477+
}
478+
479+
// ─────────────── Service ───────────────
480+
390481
@Service({ name: "MissionControl", version: 1 })
391482
class MissionControl {
392483
private _logBuffer: LogEntry[] = [];
393484
private _logResolve: ((entry: LogEntry) => void) | null = null;
394485

395-
// ... getStatus from Chapter 1 ...
486+
@Rpc()
487+
async getStatus(req: StatusRequest): Promise<StatusResponse> {
488+
return new StatusResponse({
489+
agent_id: req.agent_id,
490+
status: "running",
491+
uptime_secs: 3600,
492+
});
493+
}
396494

397495
@Rpc()
398496
async submitLog(entry: LogEntry): Promise<SubmitLogResult> {
@@ -417,19 +515,142 @@ class MissionControl {
417515
}
418516
}
419517
}
518+
519+
// ─────────────── Main ───────────────
520+
521+
async function main() {
522+
const server = new AsterServer({ services: [new MissionControl()] });
523+
await server.start();
524+
console.log(server.address); // compact aster1... address
525+
await server.serve();
526+
}
527+
528+
main();
529+
```
530+
531+
**Restart the service.** Stop the previous run with `Ctrl+C`, then start the new version:
532+
533+
```bash
534+
bun run control.ts
535+
# → aster1Qmxxxxxxxx... ← this is a NEW address; the old one is gone
420536
```
421537

538+
The address changes on every restart in dev mode (it's an ephemeral key). Copy the new address — you'll need it for both terminals below.
539+
422540
</TabItem>
423541
</LanguageTabs>
424542

543+
### Submit some logs to stream
544+
545+
`tailLogs` blocks until log entries arrive. For the demo we need a process that's actively pushing logs, so the stream has something to show. Save this as `logs.py` (or `logs.ts`) and run it in a **second terminal**:
546+
547+
<LanguageTabs>
548+
<TabItem value="python">
549+
550+
```python
551+
# logs.py — submits one log entry per second so tailLogs has something to stream.
552+
# Usage: python logs.py <aster1...address>
553+
import asyncio
554+
import sys
555+
import time
556+
from aster import AsterClient
557+
558+
async def main():
559+
if len(sys.argv) < 2:
560+
print("Usage: python logs.py <aster1...address>")
561+
sys.exit(1)
562+
563+
async with AsterClient(address=sys.argv[1]) as client:
564+
mc = client.proxy("MissionControl")
565+
566+
levels = ["info", "warn", "error"]
567+
messages = ["disk 92% full", "health check failed", "cpu spike detected"]
568+
569+
i = 0
570+
while True:
571+
await mc.submitLog({
572+
"timestamp": time.time(),
573+
"level": levels[i % len(levels)],
574+
"message": messages[i % len(messages)],
575+
"agent_id": "edge-node-7",
576+
})
577+
print(f"submitted log #{i + 1}")
578+
i += 1
579+
await asyncio.sleep(1)
580+
581+
if __name__ == "__main__":
582+
asyncio.run(main())
583+
```
584+
425585
```bash
426-
# In the shell:
586+
# Replace aster1Qm... with the address from control.py
587+
python logs.py aster1Qm...
588+
```
589+
590+
</TabItem>
591+
<TabItem value="typescript">
592+
593+
```typescript
594+
// logs.ts — submits one log entry per second so tailLogs has something to stream.
595+
// Usage: bun run logs.ts <aster1...address>
596+
import { AsterClientWrapper } from '@aster-rpc/aster';
597+
598+
async function main() {
599+
const address = process.argv[2];
600+
if (!address) {
601+
console.error("Usage: bun run logs.ts <aster1...address>");
602+
process.exit(1);
603+
}
604+
605+
const client = new AsterClientWrapper({ address });
606+
await client.connect();
607+
const mc = client.proxy("MissionControl");
608+
609+
const levels = ["info", "warn", "error"];
610+
const messages = ["disk 92% full", "health check failed", "cpu spike detected"];
611+
612+
let i = 0;
613+
while (true) {
614+
await mc.submitLog({
615+
timestamp: Date.now() / 1000,
616+
level: levels[i % levels.length],
617+
message: messages[i % messages.length],
618+
agent_id: "edge-node-7",
619+
});
620+
console.log(`submitted log #${i + 1}`);
621+
i++;
622+
await new Promise(resolve => setTimeout(resolve, 1000));
623+
}
624+
}
625+
626+
main();
627+
```
628+
629+
```bash
630+
# Replace aster1Qm... with the address from control.ts
631+
bun run logs.ts aster1Qm...
632+
```
633+
634+
</TabItem>
635+
</LanguageTabs>
636+
637+
### Tail the stream
638+
639+
In a **third terminal**, open the shell and start tailing. Replace `aster1Qm...` with the same address as before:
640+
641+
```bash
642+
aster shell aster1Qm...
643+
> cd services/MissionControl
427644
> ./tailLogs agent_id="edge-node-7" level="warn"
428645
#0 {"timestamp": 1712567890.1, "level": "warn", "message": "disk 92% full", ...}
429646
#1 {"timestamp": 1712567891.3, "level": "error", "message": "health check failed", ...}
430647
# Ctrl+C to stop
431648
```
432649

650+
You should see entries scroll past in real time as `logs.py` submits them. Note the `level="warn"` filter excludes `info` entries, so you'll see roughly two out of every three submitted logs.
651+
652+
> **Three terminals?** Yes — server (`control.py`), submitter (`logs.py`), and tail consumer (`aster shell`). That's the natural shape of any streaming demo: someone produces, someone consumes, the server brokers between them.
653+
433654
Or from your own code using the proxy client:
434655

435656
<LanguageTabs>

0 commit comments

Comments
 (0)