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,419 @@
1
+ // ============================================================================
2
+ // multiagents — Type Definitions
3
+ // ============================================================================
4
+ // Multi-agent, session, file coordination, and guardrail support.
5
+ // ============================================================================
6
+
7
+ // --- Core primitives ---
8
+
9
+ /** Unique ID for each agent instance (generated on registration, e.g. "cl-a1b2c3d4") */
10
+ export type PeerId = string;
11
+
12
+ /** Supported agent CLI types */
13
+ export type AgentType = "claude" | "codex" | "gemini" | "custom";
14
+
15
+ /** Message types for routing and formatting */
16
+ export type MessageType =
17
+ | "chat"
18
+ | "role_assignment"
19
+ | "rename"
20
+ | "broadcast"
21
+ | "team_change"
22
+ | "control"
23
+ | "system"
24
+ // Lifecycle handoff messages
25
+ | "task_complete" // agent signals their work is done, awaiting review
26
+ | "review_request" // sent to QA/reviewer to start reviewing
27
+ | "feedback" // reviewer sends actionable feedback
28
+ | "approval" // reviewer/QA approves the work
29
+ | "release"; // lead/orchestrator releases agent to disconnect
30
+
31
+ /** Session lifecycle states */
32
+ export type SessionStatus = "active" | "paused" | "archived";
33
+
34
+ /** Slot connection states */
35
+ export type SlotStatus = "connected" | "disconnected";
36
+
37
+ /**
38
+ * Task lifecycle states per slot.
39
+ * Agents cannot disconnect unless their task_state is "released".
40
+ *
41
+ * State machine:
42
+ * idle → working → done_pending_review → addressing_feedback → done_pending_review → ... → approved → released
43
+ */
44
+ export type TaskState =
45
+ | "idle" // just joined, not yet working
46
+ | "working" // actively implementing/testing/reviewing
47
+ | "done_pending_review" // signaled completion, waiting for review/QA
48
+ | "addressing_feedback" // received feedback, working on fixes
49
+ | "approved" // work approved by reviewer/QA/lead
50
+ | "released"; // cleared to disconnect
51
+
52
+ /** Guardrail trigger actions */
53
+ export type GuardrailAction = "warn" | "pause" | "stop" | "monitor";
54
+
55
+ /** Guardrail scope */
56
+ export type GuardrailScope = "session" | "per_agent";
57
+
58
+ /** File lock types */
59
+ export type LockType = "exclusive" | "shared_read";
60
+
61
+ /** File acquire result status */
62
+ export type AcquireStatus = "acquired" | "locked" | "denied" | "extended";
63
+
64
+ // --- Domain models ---
65
+
66
+ export interface Peer {
67
+ id: PeerId;
68
+ session_id: string | null;
69
+ slot_id: number | null;
70
+ pid: number;
71
+ agent_type: AgentType;
72
+ cwd: string;
73
+ git_root: string | null;
74
+ tty: string | null;
75
+ summary: string;
76
+ status: string;
77
+ registered_at: string; // ISO timestamp
78
+ last_seen: string; // ISO timestamp
79
+ }
80
+
81
+ export interface Message {
82
+ id: number;
83
+ session_id: string | null;
84
+ from_id: PeerId;
85
+ from_slot_id: number | null;
86
+ to_id: PeerId;
87
+ to_slot_id: number | null;
88
+ text: string;
89
+ msg_type: MessageType;
90
+ sent_at: string; // ISO timestamp
91
+ delivered: boolean;
92
+ delivered_at: string | null;
93
+ held: boolean;
94
+ }
95
+
96
+ export interface Session {
97
+ id: string; // slug: "auth-implementation"
98
+ name: string; // display: "Auth Implementation"
99
+ project_dir: string;
100
+ git_root: string | null;
101
+ status: SessionStatus;
102
+ pause_reason: string | null;
103
+ paused_at: number | null;
104
+ config: string; // JSON
105
+ created_at: number; // epoch ms
106
+ last_active_at: number; // epoch ms
107
+ }
108
+
109
+ export interface Slot {
110
+ id: number;
111
+ session_id: string;
112
+ agent_type: AgentType;
113
+ display_name: string | null;
114
+ role: string | null;
115
+ role_description: string | null;
116
+ role_assigned_by: string | null;
117
+ peer_id: string | null;
118
+ status: SlotStatus;
119
+ task_state: TaskState;
120
+ paused: boolean;
121
+ paused_at: number | null;
122
+ last_peer_pid: number | null;
123
+ last_connected: number | null;
124
+ last_disconnected: number | null;
125
+ context_snapshot: string | null; // JSON: { last_summary, last_status, last_cwd }
126
+ input_tokens: number;
127
+ output_tokens: number;
128
+ cache_read_tokens: number;
129
+ }
130
+
131
+ export interface FileLock {
132
+ id: number;
133
+ session_id: string;
134
+ file_path: string;
135
+ held_by_slot: number;
136
+ held_by_peer: string;
137
+ acquired_at: number;
138
+ expires_at: number;
139
+ lock_type: LockType;
140
+ purpose: string | null;
141
+ }
142
+
143
+ export interface FileOwnership {
144
+ session_id: string;
145
+ slot_id: number;
146
+ path_pattern: string;
147
+ assigned_at: number;
148
+ assigned_by: string;
149
+ }
150
+
151
+ export interface Guardrail {
152
+ id: string;
153
+ label: string;
154
+ description: string;
155
+ current_value: number;
156
+ default_value: number;
157
+ unit: string;
158
+ scope: GuardrailScope;
159
+ action: GuardrailAction;
160
+ warn_at_percent: number;
161
+ adjustable: boolean;
162
+ suggested_increases: number[];
163
+ }
164
+
165
+ export interface GuardrailState extends Guardrail {
166
+ is_overridden: boolean;
167
+ usage: {
168
+ current: number;
169
+ limit: number;
170
+ percent: number;
171
+ status: "ok" | "warning" | "triggered";
172
+ };
173
+ }
174
+
175
+ // --- Broker API request/response types ---
176
+
177
+ export interface RegisterRequest {
178
+ pid: number;
179
+ cwd: string;
180
+ git_root: string | null;
181
+ tty: string | null;
182
+ summary: string;
183
+ agent_type?: AgentType;
184
+ session_id?: string;
185
+ reconnect?: boolean;
186
+ role?: string;
187
+ display_name?: string;
188
+ slot_id?: number;
189
+ }
190
+
191
+ export interface SlotCandidate {
192
+ slot_id: number;
193
+ display_name: string | null;
194
+ role: string | null;
195
+ last_summary: string | null;
196
+ }
197
+
198
+ export interface RegisterResponse {
199
+ id: PeerId;
200
+ slot?: Slot;
201
+ recap?: Message[];
202
+ choose_slot?: SlotCandidate[];
203
+ }
204
+
205
+ export interface HeartbeatRequest {
206
+ id: PeerId;
207
+ }
208
+
209
+ export interface SetSummaryRequest {
210
+ id: PeerId;
211
+ summary: string;
212
+ }
213
+
214
+ export interface ListPeersRequest {
215
+ scope: "machine" | "directory" | "repo";
216
+ cwd: string;
217
+ git_root: string | null;
218
+ exclude_id?: PeerId;
219
+ agent_type?: AgentType | "all";
220
+ session_id?: string;
221
+ }
222
+
223
+ export interface SendMessageRequest {
224
+ from_id: PeerId;
225
+ to_id?: PeerId;
226
+ to_slot_id?: number;
227
+ text: string;
228
+ msg_type?: MessageType;
229
+ session_id?: string;
230
+ }
231
+
232
+ export interface SendMessageResult {
233
+ ok: boolean;
234
+ error?: string;
235
+ warning?: string;
236
+ }
237
+
238
+ export interface PollMessagesRequest {
239
+ id: PeerId;
240
+ }
241
+
242
+ export interface PollMessagesResponse {
243
+ messages: Message[];
244
+ paused?: boolean;
245
+ }
246
+
247
+ export interface SetRoleRequest {
248
+ peer_id: PeerId;
249
+ assigner_id: PeerId;
250
+ slot_id?: number;
251
+ role: string;
252
+ role_description: string;
253
+ }
254
+
255
+ export interface RenamePeerRequest {
256
+ peer_id: PeerId;
257
+ assigner_id: PeerId;
258
+ slot_id?: number;
259
+ display_name: string;
260
+ }
261
+
262
+ export interface AcquireFileRequest {
263
+ session_id: string;
264
+ peer_id: PeerId;
265
+ slot_id: number;
266
+ file_path: string;
267
+ purpose?: string;
268
+ timeout_ms?: number;
269
+ }
270
+
271
+ export interface AcquireFileResult {
272
+ status: AcquireStatus;
273
+ expires_at?: number;
274
+ held_by?: string;
275
+ owner?: string;
276
+ pattern?: string;
277
+ wait_estimate_ms?: number;
278
+ message: string;
279
+ }
280
+
281
+ export interface ReleaseFileRequest {
282
+ session_id: string;
283
+ peer_id: PeerId;
284
+ file_path: string;
285
+ }
286
+
287
+ export interface AssignOwnershipRequest {
288
+ session_id: string;
289
+ slot_id: number;
290
+ path_patterns: string[];
291
+ assigned_by: string;
292
+ }
293
+
294
+ export interface CreateSessionRequest {
295
+ id: string;
296
+ name: string;
297
+ project_dir: string;
298
+ git_root?: string | null;
299
+ config?: Record<string, unknown>;
300
+ }
301
+
302
+ export interface UpdateSessionRequest {
303
+ id: string;
304
+ status?: SessionStatus;
305
+ pause_reason?: string | null;
306
+ paused_at?: number | null;
307
+ config?: Record<string, unknown>;
308
+ }
309
+
310
+ export interface CreateSlotRequest {
311
+ session_id: string;
312
+ agent_type: AgentType;
313
+ display_name?: string;
314
+ role?: string;
315
+ role_description?: string;
316
+ }
317
+
318
+ export interface UpdateSlotRequest {
319
+ id: number;
320
+ paused?: boolean;
321
+ paused_at?: number | null;
322
+ status?: SlotStatus;
323
+ task_state?: TaskState;
324
+ context_snapshot?: string;
325
+ display_name?: string;
326
+ role?: string;
327
+ role_description?: string;
328
+ input_tokens?: number;
329
+ output_tokens?: number;
330
+ cache_read_tokens?: number;
331
+ }
332
+
333
+ // --- Lifecycle handoff requests ---
334
+
335
+ export interface SignalDoneRequest {
336
+ peer_id: PeerId;
337
+ session_id: string;
338
+ summary: string; // what was accomplished
339
+ }
340
+
341
+ export interface SubmitFeedbackRequest {
342
+ peer_id: PeerId;
343
+ session_id: string;
344
+ target_slot_id: number;
345
+ feedback: string;
346
+ actionable: boolean; // true = requires changes, false = informational
347
+ }
348
+
349
+ export interface ApproveRequest {
350
+ peer_id: PeerId;
351
+ session_id: string;
352
+ target_slot_id: number;
353
+ message?: string;
354
+ }
355
+
356
+ export interface ReleaseAgentRequest {
357
+ session_id: string;
358
+ target_slot_id: number;
359
+ released_by: string; // peer_id or "__orchestrator__"
360
+ message?: string;
361
+ }
362
+
363
+ export interface UnregisterResult {
364
+ ok: boolean;
365
+ denied?: boolean;
366
+ reason?: string;
367
+ task_state?: TaskState;
368
+ }
369
+
370
+ export interface UpdateGuardrailRequest {
371
+ session_id: string;
372
+ guardrail_id: string;
373
+ new_value: number;
374
+ changed_by: string;
375
+ reason?: string;
376
+ }
377
+
378
+ export interface MessageLogOptions {
379
+ limit?: number;
380
+ since?: number;
381
+ with_slot?: number;
382
+ msg_type?: MessageType;
383
+ }
384
+
385
+ // --- Buffered message (adapter-level, for piggyback delivery) ---
386
+
387
+ export interface BufferedMessage extends Message {
388
+ from_display_name?: string | null;
389
+ from_agent_type?: AgentType;
390
+ from_summary?: string | null;
391
+ from_cwd?: string;
392
+ from_role?: string | null;
393
+ }
394
+
395
+ // --- Session file (written to .multiagents/session.json) ---
396
+
397
+ export interface SessionFile {
398
+ session_id: string;
399
+ created_at: string; // ISO timestamp
400
+ broker_port: number;
401
+ }
402
+
403
+ // --- Agent launch config (for orchestrator) ---
404
+
405
+ export interface AgentLaunchConfig {
406
+ agent_type: AgentType;
407
+ name: string;
408
+ role: string;
409
+ role_description: string;
410
+ initial_task: string;
411
+ file_ownership?: string[];
412
+ report_to?: string;
413
+ }
414
+
415
+ export interface TeamConfig {
416
+ project_dir: string;
417
+ session_name: string;
418
+ agents: AgentLaunchConfig[];
419
+ }
@@ -0,0 +1,121 @@
1
+ // ============================================================================
2
+ // multiagents — Shared Utilities
3
+ // ============================================================================
4
+
5
+ import type { AgentType } from "./types.ts";
6
+ import { AGENT_ID_PREFIXES } from "./constants.ts";
7
+
8
+ /** Generate a prefixed peer ID (e.g., "cl-a1b2c3") */
9
+ export function generatePeerId(agentType: AgentType): string {
10
+ const prefix = AGENT_ID_PREFIXES[agentType] ?? "cu";
11
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
12
+ let suffix = "";
13
+ for (let i = 0; i < 6; i++) {
14
+ suffix += chars[Math.floor(Math.random() * chars.length)];
15
+ }
16
+ return `${prefix}-${suffix}`;
17
+ }
18
+
19
+ /** Format milliseconds into human-readable duration */
20
+ export function formatDuration(ms: number): string {
21
+ if (ms < 1000) return `${ms}ms`;
22
+ const seconds = Math.floor(ms / 1000);
23
+ if (seconds < 60) return `${seconds}s`;
24
+ const minutes = Math.floor(seconds / 60);
25
+ if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
26
+ const hours = Math.floor(minutes / 60);
27
+ return `${hours}h ${minutes % 60}m`;
28
+ }
29
+
30
+ /** Format "time since" a timestamp */
31
+ export function timeSince(isoOrEpoch: string | number): string {
32
+ const then =
33
+ typeof isoOrEpoch === "string"
34
+ ? new Date(isoOrEpoch).getTime()
35
+ : isoOrEpoch;
36
+ const diff = Date.now() - then;
37
+ if (diff < 0) return "just now";
38
+ return `${formatDuration(diff)} ago`;
39
+ }
40
+
41
+ /** Format epoch ms to HH:MM time string */
42
+ export function formatTime(epochOrIso: number | string): string {
43
+ const date =
44
+ typeof epochOrIso === "string"
45
+ ? new Date(epochOrIso)
46
+ : new Date(epochOrIso);
47
+ return date.toLocaleTimeString("en-US", {
48
+ hour: "2-digit",
49
+ minute: "2-digit",
50
+ hour12: false,
51
+ });
52
+ }
53
+
54
+ /** Log to stderr (stdout is reserved for MCP protocol in stdio servers) */
55
+ export function log(prefix: string, msg: string): void {
56
+ console.error(`[${prefix}] ${msg}`);
57
+ }
58
+
59
+ /** Safely parse JSON with a fallback */
60
+ export function safeJsonParse<T>(str: string | null, fallback: T): T {
61
+ if (!str) return fallback;
62
+ try {
63
+ return JSON.parse(str) as T;
64
+ } catch {
65
+ return fallback;
66
+ }
67
+ }
68
+
69
+ /** Resolve home directory in a path */
70
+ export function expandHome(path: string): string {
71
+ if (path.startsWith("~/")) {
72
+ return `${process.env.HOME}${path.slice(1)}`;
73
+ }
74
+ return path;
75
+ }
76
+
77
+ /** Truncate text to max length with ellipsis */
78
+ export function truncate(text: string, maxLen: number): string {
79
+ if (text.length <= maxLen) return text;
80
+ return text.slice(0, maxLen - 1) + "…";
81
+ }
82
+
83
+ /** Get git root for a directory */
84
+ export async function getGitRoot(cwd: string): Promise<string | null> {
85
+ try {
86
+ const proc = Bun.spawn(["git", "rev-parse", "--show-toplevel"], {
87
+ cwd,
88
+ stdout: "pipe",
89
+ stderr: "ignore",
90
+ });
91
+ const text = await new Response(proc.stdout).text();
92
+ const code = await proc.exited;
93
+ return code === 0 ? text.trim() : null;
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ /** Get parent process TTY */
100
+ export function getTty(): string | null {
101
+ try {
102
+ const ppid = process.ppid;
103
+ if (!ppid) return null;
104
+ const proc = Bun.spawnSync(["ps", "-o", "tty=", "-p", String(ppid)]);
105
+ const tty = new TextDecoder().decode(proc.stdout).trim();
106
+ return tty && tty !== "?" && tty !== "??" ? tty : null;
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ /** Convert a string to a URL-friendly slug */
113
+ export function slugify(text: string): string {
114
+ return text
115
+ .toLowerCase()
116
+ .replace(/[^a-z0-9]+/g, "-")
117
+ .replace(/^-|-$/g, "");
118
+ }
119
+
120
+ // Re-export AGENT_ID_PREFIXES for convenience
121
+ export { AGENT_ID_PREFIXES };
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false
28
+ }
29
+ }