multiagents 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.
@@ -0,0 +1,243 @@
1
+ import type {
2
+ RegisterRequest,
3
+ RegisterResponse,
4
+ HeartbeatRequest,
5
+ SetSummaryRequest,
6
+ ListPeersRequest,
7
+ Peer,
8
+ SendMessageRequest,
9
+ SendMessageResult,
10
+ PollMessagesRequest,
11
+ PollMessagesResponse,
12
+ SetRoleRequest,
13
+ RenamePeerRequest,
14
+ CreateSessionRequest,
15
+ Session,
16
+ UpdateSessionRequest,
17
+ CreateSlotRequest,
18
+ Slot,
19
+ UpdateSlotRequest,
20
+ AcquireFileRequest,
21
+ AcquireFileResult,
22
+ ReleaseFileRequest,
23
+ AssignOwnershipRequest,
24
+ FileLock,
25
+ FileOwnership,
26
+ GuardrailState,
27
+ UpdateGuardrailRequest,
28
+ MessageLogOptions,
29
+ Message,
30
+ PeerId,
31
+ SignalDoneRequest,
32
+ SubmitFeedbackRequest,
33
+ ApproveRequest,
34
+ ReleaseAgentRequest,
35
+ UnregisterResult,
36
+ } from "./types.ts";
37
+
38
+ /** HTTP client for the multiagents broker API. */
39
+ export class BrokerClient {
40
+ constructor(private baseUrl: string) {}
41
+
42
+ private async post<T>(path: string, body: unknown): Promise<T> {
43
+ const controller = new AbortController();
44
+ const timer = setTimeout(() => controller.abort(), 5000);
45
+ try {
46
+ const res = await globalThis.fetch(`${this.baseUrl}${path}`, {
47
+ method: "POST",
48
+ headers: { "Content-Type": "application/json" },
49
+ body: JSON.stringify(body),
50
+ signal: controller.signal,
51
+ });
52
+ if (!res.ok) {
53
+ const text = await res.text().catch(() => res.statusText);
54
+ throw new Error(`Broker POST ${path} failed (${res.status}): ${text}`);
55
+ }
56
+ return (await res.json()) as T;
57
+ } finally {
58
+ clearTimeout(timer);
59
+ }
60
+ }
61
+
62
+ async isAlive(): Promise<boolean> {
63
+ const controller = new AbortController();
64
+ const timer = setTimeout(() => controller.abort(), 2000);
65
+ try {
66
+ const res = await globalThis.fetch(`${this.baseUrl}/health`, {
67
+ method: "GET",
68
+ signal: controller.signal,
69
+ });
70
+ return res.ok;
71
+ } catch {
72
+ return false;
73
+ } finally {
74
+ clearTimeout(timer);
75
+ }
76
+ }
77
+
78
+ register(req: RegisterRequest): Promise<RegisterResponse> {
79
+ return this.post("/register", req);
80
+ }
81
+
82
+ heartbeat(id: PeerId): Promise<{ ok: boolean }> {
83
+ return this.post("/heartbeat", { id });
84
+ }
85
+
86
+ setSummary(id: PeerId, summary: string): Promise<{ ok: boolean }> {
87
+ return this.post("/set-summary", { id, summary });
88
+ }
89
+
90
+ listPeers(req: ListPeersRequest): Promise<Peer[]> {
91
+ return this.post("/list-peers", req);
92
+ }
93
+
94
+ sendMessage(req: SendMessageRequest): Promise<SendMessageResult> {
95
+ return this.post("/send-message", req);
96
+ }
97
+
98
+ pollMessages(id: PeerId): Promise<PollMessagesResponse> {
99
+ return this.post("/poll-messages", { id });
100
+ }
101
+
102
+ unregister(id: PeerId): Promise<UnregisterResult> {
103
+ return this.post("/unregister", { id });
104
+ }
105
+
106
+ setRole(req: SetRoleRequest): Promise<{ ok: boolean }> {
107
+ return this.post("/set-role", req);
108
+ }
109
+
110
+ renamePeer(req: RenamePeerRequest): Promise<{ ok: boolean }> {
111
+ return this.post("/rename-peer", req);
112
+ }
113
+
114
+ createSession(req: CreateSessionRequest): Promise<Session> {
115
+ return this.post("/sessions/create", req);
116
+ }
117
+
118
+ getSession(id: string): Promise<Session> {
119
+ return this.post("/sessions/get", { id });
120
+ }
121
+
122
+ listSessions(): Promise<Session[]> {
123
+ return this.post("/sessions/list", {});
124
+ }
125
+
126
+ updateSession(req: UpdateSessionRequest): Promise<Session> {
127
+ return this.post("/sessions/update", req);
128
+ }
129
+
130
+ createSlot(req: CreateSlotRequest): Promise<Slot> {
131
+ return this.post("/slots/create", req);
132
+ }
133
+
134
+ getSlot(id: number): Promise<Slot> {
135
+ return this.post("/slots/get", { id });
136
+ }
137
+
138
+ listSlots(sessionId: string): Promise<Slot[]> {
139
+ return this.post("/slots/list", { session_id: sessionId });
140
+ }
141
+
142
+ updateSlot(req: UpdateSlotRequest): Promise<Slot> {
143
+ return this.post("/slots/update", req);
144
+ }
145
+
146
+ acquireFile(req: AcquireFileRequest): Promise<AcquireFileResult> {
147
+ return this.post("/files/acquire", req);
148
+ }
149
+
150
+ releaseFile(req: ReleaseFileRequest): Promise<{ ok: boolean }> {
151
+ return this.post("/files/release", req);
152
+ }
153
+
154
+ assignOwnership(req: AssignOwnershipRequest): Promise<{ ok: boolean }> {
155
+ return this.post("/files/assign-ownership", req);
156
+ }
157
+
158
+ listFileLocks(sessionId: string): Promise<FileLock[]> {
159
+ return this.post("/files/locks", { session_id: sessionId });
160
+ }
161
+
162
+ listFileOwnership(sessionId: string): Promise<FileOwnership[]> {
163
+ return this.post("/files/ownership", { session_id: sessionId });
164
+ }
165
+
166
+ getGuardrails(sessionId: string): Promise<GuardrailState[]> {
167
+ return this.post("/guardrails", { session_id: sessionId });
168
+ }
169
+
170
+ updateGuardrail(req: UpdateGuardrailRequest): Promise<GuardrailState> {
171
+ return this.post("/guardrails/update", req);
172
+ }
173
+
174
+ getMessageLog(sessionId: string, opts?: MessageLogOptions): Promise<Message[]> {
175
+ return this.post("/message-log", { session_id: sessionId, ...opts });
176
+ }
177
+
178
+ holdMessages(sessionId: string, slotId: number): Promise<{ ok: boolean }> {
179
+ return this.post("/hold-messages", { session_id: sessionId, slot_id: slotId });
180
+ }
181
+
182
+ releaseHeldMessages(sessionId: string, slotId: number): Promise<{ ok: boolean }> {
183
+ return this.post("/release-held", { session_id: sessionId, slot_id: slotId });
184
+ }
185
+
186
+ logAgentEvent(data: { session_id: string; peer_id: PeerId; slot_id?: number; event_type: string; data?: unknown }): Promise<{ ok: boolean }> {
187
+ return this.post("/agent-event", data);
188
+ }
189
+
190
+ // --- Lifecycle handoff ---
191
+
192
+ signalDone(req: SignalDoneRequest): Promise<{ ok: boolean; task_state: string }> {
193
+ return this.post("/lifecycle/signal-done", req);
194
+ }
195
+
196
+ submitFeedback(req: SubmitFeedbackRequest): Promise<{ ok: boolean; task_state: string }> {
197
+ return this.post("/lifecycle/submit-feedback", req);
198
+ }
199
+
200
+ approve(req: ApproveRequest): Promise<{ ok: boolean; task_state: string }> {
201
+ return this.post("/lifecycle/approve", req);
202
+ }
203
+
204
+ releaseAgent(req: ReleaseAgentRequest): Promise<{ ok: boolean; task_state: string }> {
205
+ return this.post("/lifecycle/release", req);
206
+ }
207
+
208
+ getTaskState(slotId: number): Promise<{ id: number; task_state: string; display_name: string | null; role: string | null }> {
209
+ return this.post("/lifecycle/get-task-state", { slot_id: slotId });
210
+ }
211
+
212
+ // --- Plans ---
213
+
214
+ createPlan(req: { session_id: string; title: string; items: { label: string; assigned_to_slot?: number }[] }): Promise<PlanState> {
215
+ return this.post("/plan/create", req);
216
+ }
217
+
218
+ getPlan(sessionId: string): Promise<PlanState> {
219
+ return this.post("/plan/get", { session_id: sessionId });
220
+ }
221
+
222
+ updatePlanItem(req: { item_id: number; status: string; session_id?: string }): Promise<PlanState | { ok: boolean }> {
223
+ return this.post("/plan/update-item", req);
224
+ }
225
+ }
226
+
227
+ export interface PlanItem {
228
+ id: number;
229
+ plan_id: number;
230
+ parent_id: number | null;
231
+ label: string;
232
+ status: "pending" | "in_progress" | "done" | "blocked";
233
+ assigned_to_slot: number | null;
234
+ assigned_name: string | null;
235
+ completed_at: number | null;
236
+ sort_order: number;
237
+ }
238
+
239
+ export interface PlanState {
240
+ plan: { id: number; session_id: string; title: string; created_at: number; updated_at: number } | null;
241
+ items: PlanItem[];
242
+ completion: number;
243
+ }
@@ -0,0 +1,148 @@
1
+ // ============================================================================
2
+ // multiagents — Constants & Defaults
3
+ // ============================================================================
4
+
5
+ import type { AgentType, Guardrail } from "./types.ts";
6
+
7
+ // --- Networking ---
8
+
9
+ export const DEFAULT_BROKER_PORT = 7899;
10
+ export const DEFAULT_DB_PATH = `${process.env.HOME}/.multiagents/peers.db`;
11
+ export const BROKER_HOSTNAME = "127.0.0.1";
12
+
13
+ // --- Polling intervals (ms) ---
14
+
15
+ export const POLL_INTERVALS: Record<AgentType, number> = {
16
+ claude: 1000, // Claude uses channel push — polling is fallback only
17
+ codex: 300, // Aggressive: piggyback delivery depends on fast buffering
18
+ gemini: 300, // Same as Codex — no push capability
19
+ custom: 500, // Reasonable default for unknown agents
20
+ };
21
+
22
+ export const HEARTBEAT_INTERVAL = 15_000; // 15s
23
+ export const CLEANUP_INTERVAL = 30_000; // 30s
24
+ export const DASHBOARD_REFRESH = 500; // 500ms
25
+
26
+ // --- Broker startup ---
27
+
28
+ export const BROKER_STARTUP_POLL_MS = 200;
29
+ export const BROKER_STARTUP_MAX_ATTEMPTS = 30; // 30 * 200ms = 6s max wait
30
+ export const BROKER_HEALTH_TIMEOUT = 2000; // 2s timeout for health check
31
+
32
+ // --- File locks ---
33
+
34
+ export const DEFAULT_LOCK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
35
+ export const MAX_LOCK_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
36
+
37
+ // --- Sessions ---
38
+
39
+ export const SESSION_DIR = ".multiagents";
40
+ export const SESSION_FILE = ".multiagents/session.json";
41
+ export const RECONNECT_RECAP_LIMIT = 20; // messages to replay on reconnect
42
+
43
+ // --- Agent ID prefixes ---
44
+
45
+ export const AGENT_ID_PREFIXES: Record<AgentType, string> = {
46
+ claude: "cl",
47
+ codex: "cx",
48
+ gemini: "gm",
49
+ custom: "cu",
50
+ };
51
+
52
+ export const AGENT_ID_LENGTH = 8; // total chars including prefix + hyphen (e.g. "cl-a1b2c3")
53
+
54
+ // --- Guardrail defaults ---
55
+
56
+ export const DEFAULT_GUARDRAILS: Guardrail[] = [
57
+ // --- Monitoring stats (observe-only, no enforcement) ---
58
+ {
59
+ id: "session_duration",
60
+ label: "Session Duration",
61
+ description: "How long the session has been running",
62
+ current_value: 0, // no limit — monitor only
63
+ default_value: 0,
64
+ unit: "minutes",
65
+ scope: "session",
66
+ action: "monitor",
67
+ warn_at_percent: 1.0, // never warns
68
+ adjustable: false,
69
+ suggested_increases: [],
70
+ },
71
+ {
72
+ id: "messages_total",
73
+ label: "Total Messages",
74
+ description: "Total messages exchanged across all agents",
75
+ current_value: 0,
76
+ default_value: 0,
77
+ unit: "messages",
78
+ scope: "session",
79
+ action: "monitor",
80
+ warn_at_percent: 1.0,
81
+ adjustable: false,
82
+ suggested_increases: [],
83
+ },
84
+ {
85
+ id: "messages_per_agent",
86
+ label: "Messages Per Agent (max)",
87
+ description: "Highest message count from any single agent",
88
+ current_value: 0,
89
+ default_value: 0,
90
+ unit: "messages",
91
+ scope: "per_agent",
92
+ action: "monitor",
93
+ warn_at_percent: 1.0,
94
+ adjustable: false,
95
+ suggested_increases: [],
96
+ },
97
+ {
98
+ id: "agent_count",
99
+ label: "Active Agents",
100
+ description: "Currently connected agents",
101
+ current_value: 0,
102
+ default_value: 0,
103
+ unit: "agents",
104
+ scope: "session",
105
+ action: "monitor",
106
+ warn_at_percent: 1.0,
107
+ adjustable: false,
108
+ suggested_increases: [],
109
+ },
110
+ {
111
+ id: "idle_max",
112
+ label: "Longest Idle",
113
+ description: "Longest time any agent has gone without activity",
114
+ current_value: 0,
115
+ default_value: 0,
116
+ unit: "minutes",
117
+ scope: "per_agent",
118
+ action: "monitor",
119
+ warn_at_percent: 1.0,
120
+ adjustable: false,
121
+ suggested_increases: [],
122
+ },
123
+ // --- Actual guardrail (enforced) ---
124
+ {
125
+ id: "max_restarts",
126
+ label: "Restart Limit",
127
+ description:
128
+ "Stops flapping agents after too many crash-restart cycles",
129
+ current_value: 5,
130
+ default_value: 5,
131
+ unit: "restarts",
132
+ scope: "per_agent",
133
+ action: "stop",
134
+ warn_at_percent: 0.6,
135
+ adjustable: true,
136
+ suggested_increases: [8, 12, 20],
137
+ },
138
+ ];
139
+
140
+ // --- Orchestrator ---
141
+
142
+ export const STUCK_THRESHOLD_MS = 2 * 60 * 1000; // 2 min — agent considered stuck
143
+ export const SLOW_THRESHOLD_MS = 30 * 1000; // 30s — agent considered slow
144
+ export const NUDGE_WAIT_MS = 30 * 1000; // 30s after nudge before escalating
145
+ export const FLAP_WINDOW_MS = 5 * 60 * 1000; // 5 min window for flap detection
146
+ export const FLAP_THRESHOLD = 3; // crashes in window before declaring flapping
147
+ export const GUARDRAIL_CHECK_INTERVAL = 30_000; // 30s
148
+ export const CONFLICT_CHECK_INTERVAL = 10_000; // 10s
@@ -0,0 +1,97 @@
1
+ // ============================================================================
2
+ // multiagents — Auto-Summary Generation
3
+ // ============================================================================
4
+ // Generates a brief summary of what a developer is working on using git
5
+ // context (branch name, recently modified files). No LLM API calls — fast,
6
+ // free, and no hardcoded model names to maintain.
7
+ // ============================================================================
8
+
9
+ export interface SummaryContext {
10
+ cwd: string;
11
+ git_root: string | null;
12
+ git_branch?: string | null;
13
+ recent_files?: string[];
14
+ }
15
+
16
+ /**
17
+ * Generate a 1-2 sentence summary of what the agent is working on.
18
+ * Uses git context only — no external API calls.
19
+ */
20
+ export async function generateSummary(
21
+ context: SummaryContext
22
+ ): Promise<string> {
23
+ const parts: string[] = [];
24
+
25
+ if (context.git_branch && context.git_branch !== "main" && context.git_branch !== "master") {
26
+ parts.push(`Branch: ${context.git_branch}`);
27
+ }
28
+
29
+ if (context.recent_files?.length) {
30
+ const topFiles = context.recent_files.slice(0, 5).join(", ");
31
+ parts.push(`Files: ${topFiles}`);
32
+ }
33
+
34
+ if (parts.length === 0) {
35
+ const dirName = context.cwd.split("/").pop() ?? context.cwd;
36
+ return `Working in ${dirName}`;
37
+ }
38
+
39
+ return parts.join(" | ");
40
+ }
41
+
42
+ /** Get the current git branch name */
43
+ export async function getGitBranch(cwd: string): Promise<string | null> {
44
+ try {
45
+ const proc = Bun.spawn(["git", "rev-parse", "--abbrev-ref", "HEAD"], {
46
+ cwd,
47
+ stdout: "pipe",
48
+ stderr: "ignore",
49
+ });
50
+ const text = await new Response(proc.stdout).text();
51
+ const code = await proc.exited;
52
+ return code === 0 ? text.trim() : null;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /** Get recently modified tracked files in the git repo */
59
+ export async function getRecentFiles(
60
+ cwd: string,
61
+ limit = 10
62
+ ): Promise<string[]> {
63
+ try {
64
+ const diffProc = Bun.spawn(["git", "diff", "--name-only", "HEAD"], {
65
+ cwd,
66
+ stdout: "pipe",
67
+ stderr: "ignore",
68
+ });
69
+ const diffText = await new Response(diffProc.stdout).text();
70
+ await diffProc.exited;
71
+
72
+ const files = diffText
73
+ .trim()
74
+ .split("\n")
75
+ .filter((f) => f.length > 0);
76
+
77
+ if (files.length >= limit) {
78
+ return files.slice(0, limit);
79
+ }
80
+
81
+ const logProc = Bun.spawn(
82
+ ["git", "log", "--oneline", "--name-only", "-5", "--format="],
83
+ { cwd, stdout: "pipe", stderr: "ignore" }
84
+ );
85
+ const logText = await new Response(logProc.stdout).text();
86
+ await logProc.exited;
87
+
88
+ const logFiles = logText
89
+ .trim()
90
+ .split("\n")
91
+ .filter((f) => f.length > 0);
92
+
93
+ return [...new Set([...files, ...logFiles])].slice(0, limit);
94
+ } catch {
95
+ return [];
96
+ }
97
+ }