pi-famulus 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/bin/pi-famulus.js +29 -0
- package/package.json +49 -0
- package/src/agents/builtins.ts +41 -0
- package/src/agents/definition.ts +221 -0
- package/src/agents/loader.ts +119 -0
- package/src/bash-override.ts +372 -0
- package/src/behavior-guidelines.ts +42 -0
- package/src/clock.ts +169 -0
- package/src/comms/comms.ts +199 -0
- package/src/comms/mailbox.ts +156 -0
- package/src/comms/registry-host.ts +73 -0
- package/src/comms/reply-command.ts +52 -0
- package/src/comms/routing.ts +23 -0
- package/src/comms/tools.ts +339 -0
- package/src/comms/types.ts +68 -0
- package/src/config.ts +266 -0
- package/src/events.ts +61 -0
- package/src/exit-notify-gate.ts +69 -0
- package/src/exit-watchdog.ts +60 -0
- package/src/file-tail.ts +48 -0
- package/src/format.ts +276 -0
- package/src/index.ts +700 -0
- package/src/manager-client.ts +839 -0
- package/src/monitor-batching.ts +191 -0
- package/src/monitor.ts +517 -0
- package/src/native-manager.d.ts +9 -0
- package/src/native-manager.js +39 -0
- package/src/notify.ts +231 -0
- package/src/shell-exec.ts +145 -0
- package/src/subagent/agent-records.ts +168 -0
- package/src/subagent/child-bash.ts +183 -0
- package/src/subagent/conversation.ts +76 -0
- package/src/subagent/fleet-widget.ts +176 -0
- package/src/subagent/model-spec.ts +108 -0
- package/src/subagent/pi-runtime.ts +295 -0
- package/src/subagent/pool.ts +190 -0
- package/src/subagent/registry.ts +575 -0
- package/src/subagent/runner.ts +709 -0
- package/src/subagent/tool.ts +741 -0
- package/src/subagent/transcript.ts +71 -0
- package/src/subagent/types.ts +125 -0
- package/src/task-exit-notify.ts +21 -0
- package/src/task-tools.ts +330 -0
- package/src/tui/message-renderers.ts +183 -0
- package/src/tui/pi-tui-load.ts +186 -0
- package/src/tui/scroll-detail-view.ts +190 -0
- package/src/tui/task-output-paths.ts +46 -0
- package/src/tui/tasks-command.ts +515 -0
- package/src/tui/tool-component.ts +29 -0
- package/src/wake.ts +219 -0
- package/src/work-index.ts +182 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monitor output batching primitives (design doc §4.4, Appendix A).
|
|
3
|
+
*
|
|
4
|
+
* LineBatcher turns arbitrary output chunks into batched line payloads:
|
|
5
|
+
* chunks are split on newlines, complete lines are capped per line, and
|
|
6
|
+
* emissions are coalesced over a 200ms window with a per-batch size cap.
|
|
7
|
+
*
|
|
8
|
+
* RateLimiter is a token bucket used to throttle monitor event injection.
|
|
9
|
+
*/
|
|
10
|
+
import { realClock, type Clock, type ClockTimer } from "./clock";
|
|
11
|
+
|
|
12
|
+
export interface LineBatcherOptions {
|
|
13
|
+
flushMs?: number; // batching window, default 200
|
|
14
|
+
maxLineChars?: number; // per-line cap, default 500
|
|
15
|
+
maxBatchChars?: number; // per-batch cap, default 3000
|
|
16
|
+
onFlush: (text: string) => void;
|
|
17
|
+
clock?: Clock;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class LineBatcher {
|
|
21
|
+
private readonly flushMs: number;
|
|
22
|
+
private readonly maxLineChars: number;
|
|
23
|
+
private readonly maxBatchChars: number;
|
|
24
|
+
private readonly onFlush: (text: string) => void;
|
|
25
|
+
private readonly clock: Clock;
|
|
26
|
+
|
|
27
|
+
private partial = ""; // trailing bytes not yet terminated by \n
|
|
28
|
+
private lines: string[] = []; // complete lines awaiting emission
|
|
29
|
+
private timer: ClockTimer | null = null;
|
|
30
|
+
private disposed = false;
|
|
31
|
+
|
|
32
|
+
constructor(opts: LineBatcherOptions) {
|
|
33
|
+
this.flushMs = opts.flushMs ?? 200;
|
|
34
|
+
this.maxLineChars = opts.maxLineChars ?? 500;
|
|
35
|
+
this.maxBatchChars = opts.maxBatchChars ?? 3000;
|
|
36
|
+
this.onFlush = opts.onFlush;
|
|
37
|
+
this.clock = opts.clock ?? realClock;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Feed a raw output chunk (may contain no/newlines or multiple lines). */
|
|
41
|
+
push(chunk: string): void {
|
|
42
|
+
if (this.disposed || chunk.length === 0) return;
|
|
43
|
+
const combined = this.partial + chunk;
|
|
44
|
+
const parts = combined.split("\n");
|
|
45
|
+
this.partial = parts.pop() ?? "";
|
|
46
|
+
for (const line of parts) {
|
|
47
|
+
this.lines.push(this.capLine(line));
|
|
48
|
+
}
|
|
49
|
+
if (this.lines.length > 0 || this.partial.length > 0) this.schedule();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Immediately emit the current buffer when non-empty, including any
|
|
54
|
+
* unterminated trailing line. Used when a monitored process exits.
|
|
55
|
+
*/
|
|
56
|
+
flush(): void {
|
|
57
|
+
if (this.disposed) return;
|
|
58
|
+
this.clearTimer();
|
|
59
|
+
this.emit();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Clear the pending timer and drop buffered content. */
|
|
63
|
+
dispose(): void {
|
|
64
|
+
this.disposed = true;
|
|
65
|
+
this.clearTimer();
|
|
66
|
+
this.lines = [];
|
|
67
|
+
this.partial = "";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private capLine(line: string): string {
|
|
71
|
+
if (line.length <= this.maxLineChars) return line;
|
|
72
|
+
return line.slice(0, this.maxLineChars);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private schedule(): void {
|
|
76
|
+
if (this.timer !== null) return;
|
|
77
|
+
this.timer = this.clock.setTimeout(() => {
|
|
78
|
+
this.timer = null;
|
|
79
|
+
// Window end drains everything, including an unterminated trailing
|
|
80
|
+
// line (no data loss); a fresh window starts on the next push.
|
|
81
|
+
this.emit();
|
|
82
|
+
}, this.flushMs);
|
|
83
|
+
this.clock.unref?.(this.timer);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private clearTimer(): void {
|
|
87
|
+
if (this.timer !== null) {
|
|
88
|
+
this.clock.clearTimeout(this.timer);
|
|
89
|
+
this.timer = null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private emit(): void {
|
|
94
|
+
const lines = this.lines;
|
|
95
|
+
const partial = this.partial.length > 0 ? this.capLine(this.partial) : "";
|
|
96
|
+
if (lines.length === 0 && partial.length === 0) return;
|
|
97
|
+
this.lines = [];
|
|
98
|
+
this.partial = "";
|
|
99
|
+
|
|
100
|
+
let text = partial.length > 0 ? [...lines, partial].join("\n") : lines.join("\n");
|
|
101
|
+
if (text.length > this.maxBatchChars) {
|
|
102
|
+
// Keep the tail: for monitoring, the most recent lines matter most.
|
|
103
|
+
text = `…${text.slice(-(this.maxBatchChars - 1))}`;
|
|
104
|
+
}
|
|
105
|
+
if (text.length > 0) this.onFlush(text);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface SaturationWindowOptions {
|
|
110
|
+
windowMs: number;
|
|
111
|
+
dropRatio: number;
|
|
112
|
+
minimumBatches: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Tracks whether a rolling window has a sustained ratio of dropped batches. */
|
|
116
|
+
export class SaturationWindow {
|
|
117
|
+
private readonly windowMs: number;
|
|
118
|
+
private readonly dropRatio: number;
|
|
119
|
+
private readonly minimumBatches: number;
|
|
120
|
+
private samples: { at: number; dropped: boolean }[] = [];
|
|
121
|
+
|
|
122
|
+
constructor(opts: SaturationWindowOptions) {
|
|
123
|
+
this.windowMs = opts.windowMs;
|
|
124
|
+
this.dropRatio = opts.dropRatio;
|
|
125
|
+
this.minimumBatches = opts.minimumBatches;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
record(dropped: boolean, at: number): void {
|
|
129
|
+
this.prune(at);
|
|
130
|
+
this.samples.push({ at, dropped });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
isSaturated(now: number): boolean {
|
|
134
|
+
this.prune(now);
|
|
135
|
+
if (this.samples.length < this.minimumBatches) return false;
|
|
136
|
+
const first = this.samples[0];
|
|
137
|
+
// Require the retained samples to span a full window; sparse bursts alone
|
|
138
|
+
// should not be treated as sustained saturation.
|
|
139
|
+
if (!first || now - first.at < this.windowMs) return false;
|
|
140
|
+
const dropped = this.samples.reduce((count, sample) => count + Number(sample.dropped), 0);
|
|
141
|
+
return dropped / this.samples.length >= this.dropRatio;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private prune(now: number): void {
|
|
145
|
+
const cutoff = now - this.windowMs;
|
|
146
|
+
let firstLive = 0;
|
|
147
|
+
while (firstLive < this.samples.length && this.samples[firstLive].at < cutoff) firstLive++;
|
|
148
|
+
if (firstLive > 0) this.samples = this.samples.slice(firstLive);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface RateLimiterOptions {
|
|
153
|
+
capacity?: number; // default 10
|
|
154
|
+
refillIntervalMs?: number; // default 2000
|
|
155
|
+
refillAmount?: number; // default 1
|
|
156
|
+
clock?: Clock;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export class RateLimiter {
|
|
160
|
+
private readonly capacity: number;
|
|
161
|
+
private readonly refillAmount: number;
|
|
162
|
+
private tokens: number;
|
|
163
|
+
private readonly interval: ClockTimer;
|
|
164
|
+
private readonly clock: Clock;
|
|
165
|
+
private disposed = false;
|
|
166
|
+
|
|
167
|
+
constructor(opts?: RateLimiterOptions) {
|
|
168
|
+
this.capacity = opts?.capacity ?? 10;
|
|
169
|
+
this.refillAmount = opts?.refillAmount ?? 1;
|
|
170
|
+
const refillIntervalMs = opts?.refillIntervalMs ?? 2000;
|
|
171
|
+
this.clock = opts?.clock ?? realClock;
|
|
172
|
+
this.tokens = this.capacity;
|
|
173
|
+
this.interval = this.clock.setInterval(() => {
|
|
174
|
+
this.tokens = Math.min(this.capacity, this.tokens + this.refillAmount);
|
|
175
|
+
}, refillIntervalMs);
|
|
176
|
+
this.clock.unref?.(this.interval);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Consume `n` tokens (default 1); returns false when insufficient. */
|
|
180
|
+
tryConsume(n: number = 1): boolean {
|
|
181
|
+
if (this.disposed) return false;
|
|
182
|
+
if (this.tokens < n) return false;
|
|
183
|
+
this.tokens -= n;
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
dispose(): void {
|
|
188
|
+
this.disposed = true;
|
|
189
|
+
this.clock.clearInterval(this.interval);
|
|
190
|
+
}
|
|
191
|
+
}
|