taskplane 0.22.18 → 0.23.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/dashboard/public/app.js +365 -7
- package/dashboard/public/index.html +16 -0
- package/dashboard/public/style.css +105 -0
- package/dashboard/server.cjs +199 -0
- package/extensions/task-runner.ts +40 -286
- package/extensions/taskplane/abort.ts +11 -1
- package/extensions/taskplane/agent-bridge-extension.ts +159 -0
- package/extensions/taskplane/agent-host.ts +686 -0
- package/extensions/taskplane/engine.ts +75 -3
- package/extensions/taskplane/execution.ts +403 -9
- package/extensions/taskplane/extension.ts +322 -28
- package/extensions/taskplane/lane-runner.ts +567 -0
- package/extensions/taskplane/mailbox.ts +349 -1
- package/extensions/taskplane/merge.ts +208 -51
- package/extensions/taskplane/process-registry.ts +345 -0
- package/extensions/taskplane/resume.ts +185 -47
- package/extensions/taskplane/supervisor.ts +16 -12
- package/extensions/taskplane/task-executor-core.ts +553 -0
- package/extensions/taskplane/types.ts +517 -1
- package/package.json +1 -1
- package/skills/create-taskplane-task/SKILL.md +41 -33
- package/skills/create-taskplane-task/references/prompt-template.md +3 -3
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { join, dirname } from "path";
|
|
27
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, renameSync, unlinkSync } from "fs";
|
|
27
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, renameSync, unlinkSync, appendFileSync } from "fs";
|
|
28
28
|
import { randomBytes } from "crypto";
|
|
29
29
|
import type { MailboxMessage, MailboxMessageType, WriteMailboxMessageOpts } from "./types.ts";
|
|
30
30
|
import { MAILBOX_DIR_NAME, MAILBOX_MAX_CONTENT_BYTES, MAILBOX_MESSAGE_TYPES } from "./types.ts";
|
|
@@ -339,3 +339,351 @@ export function isValidMailboxMessage(obj: unknown): obj is MailboxMessage {
|
|
|
339
339
|
typeof m.content === "string"
|
|
340
340
|
);
|
|
341
341
|
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
// ── Outbox (Agent → Supervisor, TP-106) ─────────────────────────
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Outbox directory for a specific agent session.
|
|
348
|
+
*
|
|
349
|
+
* @param stateRoot - Root directory containing .pi/
|
|
350
|
+
* @param batchId - Batch ID
|
|
351
|
+
* @param sessionName - Agent ID / session name
|
|
352
|
+
* @returns Absolute path: `{stateRoot}/.pi/mailbox/{batchId}/{sessionName}/outbox/`
|
|
353
|
+
*
|
|
354
|
+
* @since TP-106
|
|
355
|
+
*/
|
|
356
|
+
export function sessionOutboxDir(stateRoot: string, batchId: string, sessionName: string): string {
|
|
357
|
+
return join(stateRoot, ".pi", MAILBOX_DIR_NAME, batchId, sessionName, "outbox");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Write a reply or escalation message to an agent's outbox.
|
|
362
|
+
*
|
|
363
|
+
* Used by agents (via bridge tools or direct write) to communicate
|
|
364
|
+
* back to the supervisor. The engine or lane-runner polls outbox
|
|
365
|
+
* directories and surfaces messages as supervisor alerts.
|
|
366
|
+
*
|
|
367
|
+
* @param stateRoot - Root directory containing .pi/
|
|
368
|
+
* @param batchId - Current batch ID
|
|
369
|
+
* @param from - Agent ID writing the message
|
|
370
|
+
* @param opts - Message content and metadata
|
|
371
|
+
* @returns The written MailboxMessage
|
|
372
|
+
*
|
|
373
|
+
* @since TP-106
|
|
374
|
+
*/
|
|
375
|
+
export function writeOutboxMessage(
|
|
376
|
+
stateRoot: string,
|
|
377
|
+
batchId: string,
|
|
378
|
+
from: string,
|
|
379
|
+
opts: WriteMailboxMessageOpts,
|
|
380
|
+
): MailboxMessage {
|
|
381
|
+
const outboxDir = sessionOutboxDir(stateRoot, batchId, from);
|
|
382
|
+
mkdirSync(outboxDir, { recursive: true });
|
|
383
|
+
|
|
384
|
+
const contentBytes = Buffer.byteLength(opts.content, "utf8");
|
|
385
|
+
if (contentBytes > MAILBOX_MAX_CONTENT_BYTES) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
`Outbox message content exceeds ${MAILBOX_MAX_CONTENT_BYTES} byte limit (${contentBytes} bytes).`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const timestamp = Date.now();
|
|
392
|
+
const nonce = randomBytes(3).toString("hex").slice(0, 5);
|
|
393
|
+
const id = `${timestamp}-${nonce}`;
|
|
394
|
+
|
|
395
|
+
const message: MailboxMessage = {
|
|
396
|
+
id,
|
|
397
|
+
batchId,
|
|
398
|
+
from,
|
|
399
|
+
to: "supervisor",
|
|
400
|
+
timestamp,
|
|
401
|
+
type: opts.type,
|
|
402
|
+
content: opts.content,
|
|
403
|
+
expectsReply: opts.expectsReply ?? false,
|
|
404
|
+
replyTo: opts.replyTo ?? null,
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const finalFilename = `${id}.msg.json`;
|
|
408
|
+
const tempFilename = `${id}.msg.json.tmp`;
|
|
409
|
+
const tempPath = join(outboxDir, tempFilename);
|
|
410
|
+
const finalPath = join(outboxDir, finalFilename);
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
writeFileSync(tempPath, JSON.stringify(message, null, 2) + "\n", "utf-8");
|
|
414
|
+
renameSync(tempPath, finalPath);
|
|
415
|
+
} catch (err) {
|
|
416
|
+
try { if (existsSync(tempPath)) unlinkSync(tempPath); } catch { /* cleanup */ }
|
|
417
|
+
throw new Error(`Failed to write outbox message: ${err instanceof Error ? err.message : String(err)}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return message;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Read pending outbox messages from an agent's outbox directory.
|
|
425
|
+
*
|
|
426
|
+
* @param stateRoot - Root directory containing .pi/
|
|
427
|
+
* @param batchId - Batch ID
|
|
428
|
+
* @param agentId - Agent ID whose outbox to read
|
|
429
|
+
* @returns Array of outbox messages sorted by timestamp
|
|
430
|
+
*
|
|
431
|
+
* @since TP-106
|
|
432
|
+
*/
|
|
433
|
+
export function readOutbox(
|
|
434
|
+
stateRoot: string,
|
|
435
|
+
batchId: string,
|
|
436
|
+
agentId: string,
|
|
437
|
+
): MailboxMessage[] {
|
|
438
|
+
const outboxDir = sessionOutboxDir(stateRoot, batchId, agentId);
|
|
439
|
+
if (!existsSync(outboxDir)) return [];
|
|
440
|
+
|
|
441
|
+
let entries: string[];
|
|
442
|
+
try {
|
|
443
|
+
entries = readdirSync(outboxDir);
|
|
444
|
+
} catch {
|
|
445
|
+
return [];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const msgFiles = entries.filter(f => f.endsWith(".msg.json") && !f.endsWith(".msg.json.tmp"));
|
|
449
|
+
const messages: MailboxMessage[] = [];
|
|
450
|
+
|
|
451
|
+
for (const filename of msgFiles) {
|
|
452
|
+
try {
|
|
453
|
+
const raw = readFileSync(join(outboxDir, filename), "utf-8");
|
|
454
|
+
const parsed = JSON.parse(raw);
|
|
455
|
+
if (isValidMailboxMessage(parsed)) {
|
|
456
|
+
messages.push(parsed);
|
|
457
|
+
}
|
|
458
|
+
} catch { /* skip malformed */ }
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
messages.sort((a, b) => a.timestamp - b.timestamp);
|
|
462
|
+
return messages;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Read all outbox messages (pending + processed) for durable history.
|
|
467
|
+
*
|
|
468
|
+
* Unlike readOutbox() which only reads pending messages, this function
|
|
469
|
+
* also reads outbox/processed/ so consumed replies remain visible to
|
|
470
|
+
* the supervisor via read_agent_replies.
|
|
471
|
+
*
|
|
472
|
+
* @param stateRoot - Root directory containing .pi/
|
|
473
|
+
* @param batchId - Batch ID
|
|
474
|
+
* @param agentId - Agent ID whose outbox history to read
|
|
475
|
+
* @returns Array of { message, acked } sorted by timestamp
|
|
476
|
+
*
|
|
477
|
+
* @since TP-091
|
|
478
|
+
*/
|
|
479
|
+
export function readOutboxHistory(
|
|
480
|
+
stateRoot: string,
|
|
481
|
+
batchId: string,
|
|
482
|
+
agentId: string,
|
|
483
|
+
): Array<{ message: MailboxMessage; acked: boolean }> {
|
|
484
|
+
const outboxDir = sessionOutboxDir(stateRoot, batchId, agentId);
|
|
485
|
+
const results: Array<{ message: MailboxMessage; acked: boolean }> = [];
|
|
486
|
+
|
|
487
|
+
for (const [dir, acked] of [[outboxDir, false], [join(outboxDir, "processed"), true]] as const) {
|
|
488
|
+
if (!existsSync(dir)) continue;
|
|
489
|
+
let entries: string[];
|
|
490
|
+
try { entries = readdirSync(dir); } catch { continue; }
|
|
491
|
+
|
|
492
|
+
const msgFiles = entries.filter(f => f.endsWith(".msg.json") && !f.endsWith(".msg.json.tmp"));
|
|
493
|
+
for (const filename of msgFiles) {
|
|
494
|
+
try {
|
|
495
|
+
const raw = readFileSync(join(dir, filename), "utf-8");
|
|
496
|
+
const parsed = JSON.parse(raw);
|
|
497
|
+
if (isValidMailboxMessage(parsed)) {
|
|
498
|
+
results.push({ message: parsed, acked });
|
|
499
|
+
}
|
|
500
|
+
} catch { /* skip malformed */ }
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
results.sort((a, b) => a.message.timestamp - b.message.timestamp);
|
|
505
|
+
return results;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Ack (consume) a specific outbox message by moving it to processed/.
|
|
510
|
+
*
|
|
511
|
+
* Returns false if the message is already gone (race-safe/idempotent).
|
|
512
|
+
*
|
|
513
|
+
* @since TP-106
|
|
514
|
+
*/
|
|
515
|
+
export function ackOutboxMessage(
|
|
516
|
+
stateRoot: string,
|
|
517
|
+
batchId: string,
|
|
518
|
+
agentId: string,
|
|
519
|
+
messageId: string,
|
|
520
|
+
): boolean {
|
|
521
|
+
const outboxDir = sessionOutboxDir(stateRoot, batchId, agentId);
|
|
522
|
+
const processedDir = join(outboxDir, "processed");
|
|
523
|
+
const file = `${messageId}.msg.json`;
|
|
524
|
+
const srcPath = join(outboxDir, file);
|
|
525
|
+
const dstPath = join(processedDir, file);
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
mkdirSync(processedDir, { recursive: true });
|
|
529
|
+
renameSync(srcPath, dstPath);
|
|
530
|
+
return true;
|
|
531
|
+
} catch (err: unknown) {
|
|
532
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
533
|
+
if (code === "ENOENT") return false;
|
|
534
|
+
process.stderr.write(
|
|
535
|
+
`[mailbox] WARNING: failed to ack outbox ${file}: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
536
|
+
);
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Discover all agent IDs that have mailbox directories for a batch.
|
|
543
|
+
* Returns directory names under .pi/mailbox/{batchId}/ excluding _broadcast.
|
|
544
|
+
* Used to find agents with historical messages even if no longer in the registry.
|
|
545
|
+
*
|
|
546
|
+
* @param stateRoot - Root directory containing .pi/
|
|
547
|
+
* @param batchId - Batch ID
|
|
548
|
+
* @returns Array of agent IDs found in mailbox directories
|
|
549
|
+
*
|
|
550
|
+
* @since TP-091
|
|
551
|
+
*/
|
|
552
|
+
export function discoverMailboxAgentIds(
|
|
553
|
+
stateRoot: string,
|
|
554
|
+
batchId: string,
|
|
555
|
+
): string[] {
|
|
556
|
+
const mbRoot = join(stateRoot, ".pi", MAILBOX_DIR_NAME, batchId);
|
|
557
|
+
if (!existsSync(mbRoot)) return [];
|
|
558
|
+
try {
|
|
559
|
+
const entries = readdirSync(mbRoot, { withFileTypes: true });
|
|
560
|
+
return entries
|
|
561
|
+
.filter(e => e.isDirectory() && e.name !== "_broadcast")
|
|
562
|
+
.map(e => e.name);
|
|
563
|
+
} catch {
|
|
564
|
+
return [];
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
export type MailboxAuditEventType =
|
|
570
|
+
| "message_sent"
|
|
571
|
+
| "message_delivered"
|
|
572
|
+
| "message_replied"
|
|
573
|
+
| "message_escalated"
|
|
574
|
+
| "message_rate_limited";
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Append a mailbox audit event to .pi/mailbox/{batchId}/events.jsonl.
|
|
578
|
+
*
|
|
579
|
+
* Best-effort: logs warning but never throws.
|
|
580
|
+
*
|
|
581
|
+
* @since TP-106
|
|
582
|
+
*/
|
|
583
|
+
export function appendMailboxAuditEvent(
|
|
584
|
+
stateRoot: string,
|
|
585
|
+
batchId: string,
|
|
586
|
+
event: {
|
|
587
|
+
type: MailboxAuditEventType;
|
|
588
|
+
ts?: number;
|
|
589
|
+
from?: string;
|
|
590
|
+
to?: string;
|
|
591
|
+
messageId?: string;
|
|
592
|
+
messageType?: string;
|
|
593
|
+
contentPreview?: string;
|
|
594
|
+
broadcast?: boolean;
|
|
595
|
+
reason?: string;
|
|
596
|
+
retryAfterMs?: number;
|
|
597
|
+
},
|
|
598
|
+
): void {
|
|
599
|
+
const eventsPath = join(mailboxRoot(stateRoot, batchId), "events.jsonl");
|
|
600
|
+
try {
|
|
601
|
+
mkdirSync(dirname(eventsPath), { recursive: true });
|
|
602
|
+
appendFileSync(
|
|
603
|
+
eventsPath,
|
|
604
|
+
JSON.stringify({ batchId, ts: event.ts ?? Date.now(), ...event }) + "\n",
|
|
605
|
+
"utf-8",
|
|
606
|
+
);
|
|
607
|
+
} catch (err) {
|
|
608
|
+
process.stderr.write(
|
|
609
|
+
`[mailbox] WARNING: failed to append mailbox event: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
// ── Broadcast (TP-106) ────────────────────────────────────────
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Write a broadcast message to all agents.
|
|
619
|
+
*
|
|
620
|
+
* The message is written to `_broadcast/inbox/`. Agent hosts check
|
|
621
|
+
* this directory alongside their own inbox on each `message_end`.
|
|
622
|
+
*
|
|
623
|
+
* @param stateRoot - Root directory containing .pi/
|
|
624
|
+
* @param batchId - Current batch ID
|
|
625
|
+
* @param opts - Message content and metadata
|
|
626
|
+
* @returns The written MailboxMessage
|
|
627
|
+
*
|
|
628
|
+
* @since TP-106
|
|
629
|
+
*/
|
|
630
|
+
export function writeBroadcastMessage(
|
|
631
|
+
stateRoot: string,
|
|
632
|
+
batchId: string,
|
|
633
|
+
opts: WriteMailboxMessageOpts,
|
|
634
|
+
): MailboxMessage {
|
|
635
|
+
return writeMailboxMessage(stateRoot, batchId, "_broadcast", {
|
|
636
|
+
...opts,
|
|
637
|
+
from: opts.from || "supervisor",
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
// ── Rate Limiting (TP-106) ─────────────────────────────────────
|
|
643
|
+
|
|
644
|
+
/** Default rate limit: max 1 message per agent per 30 seconds. */
|
|
645
|
+
export const RATE_LIMIT_WINDOW_MS = 30_000;
|
|
646
|
+
|
|
647
|
+
/** In-memory rate limit tracker. Keyed by target agent ID. */
|
|
648
|
+
const rateLimitTracker = new Map<string, number>();
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Check whether sending a message to a target is rate-limited.
|
|
652
|
+
*
|
|
653
|
+
* @param targetAgentId - Agent ID being sent to
|
|
654
|
+
* @param windowMs - Rate limit window in ms (default: 30_000)
|
|
655
|
+
* @returns Object with `allowed` and optional `retryAfterMs`
|
|
656
|
+
*
|
|
657
|
+
* @since TP-106
|
|
658
|
+
*/
|
|
659
|
+
export function checkRateLimit(
|
|
660
|
+
targetAgentId: string,
|
|
661
|
+
windowMs: number = RATE_LIMIT_WINDOW_MS,
|
|
662
|
+
): { allowed: boolean; retryAfterMs?: number } {
|
|
663
|
+
const lastSent = rateLimitTracker.get(targetAgentId);
|
|
664
|
+
if (!lastSent) return { allowed: true };
|
|
665
|
+
|
|
666
|
+
const elapsed = Date.now() - lastSent;
|
|
667
|
+
if (elapsed >= windowMs) return { allowed: true };
|
|
668
|
+
|
|
669
|
+
return { allowed: false, retryAfterMs: windowMs - elapsed };
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Record a send timestamp for rate limiting.
|
|
674
|
+
*
|
|
675
|
+
* @param targetAgentId - Agent ID that was sent to
|
|
676
|
+
*
|
|
677
|
+
* @since TP-106
|
|
678
|
+
*/
|
|
679
|
+
export function recordSend(targetAgentId: string): void {
|
|
680
|
+
rateLimitTracker.set(targetAgentId, Date.now());
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Reset rate limit state (for testing).
|
|
685
|
+
* @since TP-106
|
|
686
|
+
*/
|
|
687
|
+
export function _resetRateLimits(): void {
|
|
688
|
+
rateLimitTracker.clear();
|
|
689
|
+
}
|