@buihongduc132/pi-acp-agents 0.3.1
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/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +359 -0
- package/index.ts +1521 -0
- package/package.json +103 -0
- package/skills/pi-acp-agents/SKILL.md +112 -0
- package/src/acp-widget.ts +379 -0
- package/src/adapter-factory.ts +55 -0
- package/src/adapters/acpx.ts +215 -0
- package/src/adapters/base.ts +117 -0
- package/src/adapters/codex.ts +77 -0
- package/src/adapters/custom.ts +14 -0
- package/src/adapters/gemini.ts +66 -0
- package/src/adapters/opencode.ts +101 -0
- package/src/config/config.ts +312 -0
- package/src/config/types.ts +203 -0
- package/src/coordination/alias-resolver.ts +208 -0
- package/src/coordination/coordinator.ts +266 -0
- package/src/coordination/worker-dispatcher.ts +191 -0
- package/src/core/async-executor.ts +149 -0
- package/src/core/circuit-breaker.ts +254 -0
- package/src/core/client.ts +661 -0
- package/src/core/health-monitor.ts +200 -0
- package/src/core/protocol-validator.ts +259 -0
- package/src/core/session-lifecycle.ts +46 -0
- package/src/core/session-manager.ts +64 -0
- package/src/extension-safety.ts +200 -0
- package/src/logger.ts +92 -0
- package/src/management/event-log.ts +31 -0
- package/src/management/governance-store.ts +123 -0
- package/src/management/heartbeat-parser.ts +92 -0
- package/src/management/mailbox-manager.ts +95 -0
- package/src/management/runtime-paths.ts +34 -0
- package/src/management/safe-mkdir.ts +78 -0
- package/src/management/session-archive-store.ts +136 -0
- package/src/management/session-name-store.ts +88 -0
- package/src/management/task-store.ts +260 -0
- package/src/management/worker-store.ts +164 -0
- package/src/public-api.ts +72 -0
- package/src/settings/agent-config-tui.ts +456 -0
- package/src/settings/agents-command.ts +138 -0
- package/src/settings/config.ts +201 -0
- package/src/settings/configure-tui.ts +135 -0
package/package.json
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@buihongduc132/pi-acp-agents",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Pi extension: ACP agent client — spawn and control ACP-compatible agents (Gemini CLI, etc.) from within pi",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"pi",
|
|
9
|
+
"pi-coding-agent",
|
|
10
|
+
"acp",
|
|
11
|
+
"agent-client-protocol",
|
|
12
|
+
"multi-agent",
|
|
13
|
+
"pi",
|
|
14
|
+
"pi-coding-agent"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./index.ts",
|
|
18
|
+
"types": "./src/public-api.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./src/public-api.ts",
|
|
22
|
+
"import": "./index.ts"
|
|
23
|
+
},
|
|
24
|
+
"./api": {
|
|
25
|
+
"types": "./src/public-api.ts",
|
|
26
|
+
"import": "./src/public-api.ts"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"author": "buihongduc132",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/buihongduc132/pi-acp-agents.git"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/buihongduc132/pi-acp-agents#readme",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/buihongduc132/pi-acp-agents/issues"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"index.ts",
|
|
41
|
+
"src/**/*.ts",
|
|
42
|
+
"skills/**/*",
|
|
43
|
+
"README.md",
|
|
44
|
+
"CHANGELOG.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"pi": {
|
|
48
|
+
"extensions": [
|
|
49
|
+
"./index.ts"
|
|
50
|
+
],
|
|
51
|
+
"skills": [
|
|
52
|
+
"./skills"
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
57
|
+
"typebox": "*"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"@mariozechner/pi-coding-agent": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"typebox": {
|
|
64
|
+
"optional": true
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"workspaces": [
|
|
68
|
+
"packages/*"
|
|
69
|
+
],
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"@agentclientprotocol/sdk": "^0.21.0",
|
|
72
|
+
"pi-acp-types": "workspace:*"
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"@mariozechner/pi-coding-agent": "^0.73.0",
|
|
76
|
+
"@mariozechner/pi-tui": "^0.73.0",
|
|
77
|
+
"@types/node": "^22.0.0",
|
|
78
|
+
"@vitest/coverage-v8": "^4.1.5",
|
|
79
|
+
"typebox": "^1.1.37",
|
|
80
|
+
"typescript": "^6.0.3",
|
|
81
|
+
"vitest": "^4.1.5"
|
|
82
|
+
},
|
|
83
|
+
"scripts": {
|
|
84
|
+
"test": "vitest run",
|
|
85
|
+
"test:ci": "vitest run --coverage",
|
|
86
|
+
"test:watch": "vitest",
|
|
87
|
+
"typecheck": "tsc --noEmit",
|
|
88
|
+
"prepublishOnly": "npm run test:ci",
|
|
89
|
+
"prepack": "node scripts/prepack.mjs",
|
|
90
|
+
"postpack": "node scripts/postpack.mjs",
|
|
91
|
+
"version": "node scripts/version-sync.mjs",
|
|
92
|
+
"release:patch": "npm version patch -m 'release: v%s'",
|
|
93
|
+
"release:minor": "npm version minor -m 'release: v%s'",
|
|
94
|
+
"release:major": "npm version major -m 'release: v%s'",
|
|
95
|
+
"release:beta": "npm version prerelease --preid=beta -m 'release: v%s'",
|
|
96
|
+
"publish:beta": "npm publish --tag beta --access public",
|
|
97
|
+
"publish:next": "npm publish --tag next --access public",
|
|
98
|
+
"publish:dry": "npm publish --dry-run --access public"
|
|
99
|
+
},
|
|
100
|
+
"engines": {
|
|
101
|
+
"node": ">=22.0.0"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pi-acp-agents
|
|
3
|
+
description: Use when working with the pi-acp-agents package, its ACP tool surface, ACP agent session lifecycle, or ACP configuration and runtime behavior inside pi.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# pi-acp-agents
|
|
7
|
+
|
|
8
|
+
ACP agent client for pi — spawn and control ACP-compatible agents from within pi.
|
|
9
|
+
|
|
10
|
+
## What it does
|
|
11
|
+
|
|
12
|
+
This pi extension registers tools that let the pi LLM communicate with external ACP (Agent Client Protocol) compatible agents like Gemini CLI, Claude, or any custom command that speaks ACP over stdio JSON-RPC.
|
|
13
|
+
|
|
14
|
+
## Tools
|
|
15
|
+
|
|
16
|
+
| Tool | Description |
|
|
17
|
+
| ----------------------- | -------------------------------------------------------------- |
|
|
18
|
+
| `acp_prompt` | Send a prompt to an ACP agent, get the text response |
|
|
19
|
+
| `acp_status` | Show configured agents, active sessions, circuit breaker state |
|
|
20
|
+
| `acp_session_new` | Create a new isolated session with an agent |
|
|
21
|
+
| `acp_session_load` | Load an existing session by ID |
|
|
22
|
+
| `acp_session_set_model` | Change the model for an active session |
|
|
23
|
+
| `acp_session_set_mode` | Change the mode (thinking level) for an active session |
|
|
24
|
+
| `acp_cancel` | Cancel an ongoing prompt |
|
|
25
|
+
| `acp_delegate` | Delegate a task (short-lived session, auto-disposed) |
|
|
26
|
+
| `acp_broadcast` | Send same prompt to multiple agents in parallel |
|
|
27
|
+
| `acp_compare` | Get responses from multiple agents and compare them |
|
|
28
|
+
|
|
29
|
+
## Configuration
|
|
30
|
+
|
|
31
|
+
Config file: `~/.pi/acp-agents/config.json`
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"agent_servers": {
|
|
36
|
+
"gemini": {
|
|
37
|
+
"command": "gemini",
|
|
38
|
+
"args": ["--acp"],
|
|
39
|
+
"default_model": "gemini-2.5-pro"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"defaultAgent": "gemini"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Fields
|
|
47
|
+
|
|
48
|
+
| Field | Default | Description |
|
|
49
|
+
| --------------------------- | ------------------- | ----------------------------------------- |
|
|
50
|
+
| `agent_servers` | `{ gemini: {...} }` | Map of agent name → config |
|
|
51
|
+
| `defaultAgent` | `"gemini"` | Agent used when not specified |
|
|
52
|
+
| `staleTimeoutMs` | `3600000` (1 hour) | Session auto-close timeout for stalled-no-response and completed-idle |
|
|
53
|
+
| `healthCheckIntervalMs` | `30000` (30s) | Background health polling interval |
|
|
54
|
+
| `circuitBreakerMaxFailures` | `3` | Consecutive failures before circuit opens |
|
|
55
|
+
| `circuitBreakerResetMs` | `60000` (60s) | Time before circuit half-opens |
|
|
56
|
+
| `stallTimeoutMs` | `3600000` (1 hour) | Per-operation timeout |
|
|
57
|
+
|
|
58
|
+
### Per-agent config
|
|
59
|
+
|
|
60
|
+
| Field | Required | Description |
|
|
61
|
+
| -------------- | -------- | ----------------------------- |
|
|
62
|
+
| `command` | **yes** | Executable to spawn |
|
|
63
|
+
| `args` | no | Arguments (e.g., `["--acp"]`) |
|
|
64
|
+
| `env` | no | Extra environment variables |
|
|
65
|
+
| `cwd` | no | Working directory override |
|
|
66
|
+
| `default_model` | no | Default model ID |
|
|
67
|
+
|
|
68
|
+
## Resilience
|
|
69
|
+
|
|
70
|
+
- **Circuit breaker**: Opens after N consecutive failures, auto-recovers after timeout
|
|
71
|
+
- **Stall timeout**: Prompts that receive no activity are auto-cancelled
|
|
72
|
+
- **Health polling**: Background monitor enforces separate `lastResponseAt` and `completedAt` 1-hour auto-close policies
|
|
73
|
+
- **Busy mutex**: Prevents concurrent prompts on same session
|
|
74
|
+
- **Process safeguards**: SIGTERM → SIGKILL escalation, EPIPE error handlers
|
|
75
|
+
- **Non-blocking**: All tool errors return as tool error results, never throw
|
|
76
|
+
|
|
77
|
+
## Architecture
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
Adapter pattern:
|
|
81
|
+
AcpAgentAdapter (abstract base)
|
|
82
|
+
├── GeminiAcpAdapter — gemini --acp defaults
|
|
83
|
+
└── CustomAcpAdapter — any user-defined ACP command
|
|
84
|
+
|
|
85
|
+
Client layer:
|
|
86
|
+
AcpClient — wraps @agentclientprotocol/sdk ClientSideConnection
|
|
87
|
+
|
|
88
|
+
Resilience:
|
|
89
|
+
AcpCircuitBreaker — wraps all tool execute calls
|
|
90
|
+
HealthMonitor — background session health polling
|
|
91
|
+
|
|
92
|
+
Coordination:
|
|
93
|
+
AgentCoordinator — multi-agent delegate/broadcast/compare
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Logs
|
|
97
|
+
|
|
98
|
+
Central logs: `~/.pi/acp-agents/logs/`
|
|
99
|
+
|
|
100
|
+
- `main.log` — general log
|
|
101
|
+
- `session-{id}/trace.jsonl` — per-session JSON-RPC traces
|
|
102
|
+
|
|
103
|
+
## Prerequisites
|
|
104
|
+
|
|
105
|
+
- Gemini CLI installed and authenticated: `gemini --version` + `gemini` (login)
|
|
106
|
+
- ACP TypeScript SDK: bundled as dependency
|
|
107
|
+
|
|
108
|
+
## References
|
|
109
|
+
|
|
110
|
+
- ACP Protocol: https://agentclientprotocol.com/protocol/overview
|
|
111
|
+
- ACP TypeScript SDK: `@agentclientprotocol/sdk`
|
|
112
|
+
- Gemini ACP docs: https://geminicli.com/docs/cli/acp-mode/
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP TUI Widget — persistent panel for ACP agent status
|
|
3
|
+
*
|
|
4
|
+
* Renders in pi's TUI status area, similar to pi-agent-teams widget.
|
|
5
|
+
* Shows: header, circuit breaker state, per-session rows, totals, hints.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Theme, ThemeColor } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import type { Component } from "@mariozechner/pi-tui";
|
|
10
|
+
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
11
|
+
|
|
12
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export type AcpSessionStatus = "active" | "idle" | "stale" | "error";
|
|
15
|
+
|
|
16
|
+
export interface AcpWidgetSession {
|
|
17
|
+
sessionId: string;
|
|
18
|
+
sessionName?: string;
|
|
19
|
+
agentName: string;
|
|
20
|
+
cwd: string;
|
|
21
|
+
status: AcpSessionStatus;
|
|
22
|
+
lastActivityAt: Date;
|
|
23
|
+
createdAt: Date;
|
|
24
|
+
model?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AcpWidgetDelegation {
|
|
28
|
+
id: string;
|
|
29
|
+
agentName: string;
|
|
30
|
+
phase: string;
|
|
31
|
+
startedAt: Date;
|
|
32
|
+
lastActivityAt: Date;
|
|
33
|
+
text?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AcpDelegationHistoryEntry {
|
|
37
|
+
agentName: string;
|
|
38
|
+
status: "completed" | "error";
|
|
39
|
+
error?: string;
|
|
40
|
+
sessionId?: string;
|
|
41
|
+
finishedAt: Date;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AcpWidgetActivity {
|
|
45
|
+
activeDelegations: number;
|
|
46
|
+
activeBroadcasts: number;
|
|
47
|
+
activeCompares: number;
|
|
48
|
+
delegations: AcpWidgetDelegation[];
|
|
49
|
+
/** Capped at 20 entries — most recent last. */
|
|
50
|
+
delegationHistory?: AcpDelegationHistoryEntry[];
|
|
51
|
+
lastError?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface AcpWidgetWorker {
|
|
55
|
+
name: string;
|
|
56
|
+
agentName: string;
|
|
57
|
+
status: string;
|
|
58
|
+
tokenCountTotal: number;
|
|
59
|
+
toolCallCount: number;
|
|
60
|
+
ageSeconds: number;
|
|
61
|
+
stale: boolean;
|
|
62
|
+
currentTaskId?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface AcpWidgetState {
|
|
66
|
+
sessions: AcpWidgetSession[];
|
|
67
|
+
circuitBreakerState: "closed" | "open" | "half-open";
|
|
68
|
+
configuredAgentNames: string[];
|
|
69
|
+
configuredAliases?: string[];
|
|
70
|
+
defaultAgent?: string;
|
|
71
|
+
activity: AcpWidgetActivity;
|
|
72
|
+
workers?: AcpWidgetWorker[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Status styling ──────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
const STATUS_ICON: Record<AcpSessionStatus, string> = {
|
|
78
|
+
active: "●",
|
|
79
|
+
idle: "○",
|
|
80
|
+
stale: "◻",
|
|
81
|
+
error: "✕",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const STATUS_COLOR: Record<AcpSessionStatus, ThemeColor> = {
|
|
85
|
+
active: "success",
|
|
86
|
+
idle: "muted",
|
|
87
|
+
stale: "warning",
|
|
88
|
+
error: "error",
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const CB_ICON: Record<string, { icon: string; color: ThemeColor }> = {
|
|
92
|
+
closed: { icon: "●", color: "success" },
|
|
93
|
+
open: { icon: "✕", color: "error" },
|
|
94
|
+
"half-open": { icon: "◐", color: "warning" },
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const WORKER_STATUS_ICON: Record<string, { icon: string; color: ThemeColor }> = {
|
|
98
|
+
online: { icon: "●", color: "success" },
|
|
99
|
+
idle: { icon: "○", color: "muted" },
|
|
100
|
+
busy: { icon: "●", color: "accent" },
|
|
101
|
+
offline: { icon: "✕", color: "dim" },
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
/** Pad a string to a visible width, accounting for ANSI escape codes. */
|
|
107
|
+
function padRight(text: string, width: number): string {
|
|
108
|
+
const vw = visibleWidth(text);
|
|
109
|
+
return vw >= width ? text : text + " ".repeat(width - vw);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Format token count with K/M suffix. */
|
|
113
|
+
function formatTokens(count: number): string {
|
|
114
|
+
if (count === 0) return "0";
|
|
115
|
+
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
116
|
+
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`;
|
|
117
|
+
return String(count);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Short session ID — first 8 chars. */
|
|
121
|
+
function shortId(id: string): string {
|
|
122
|
+
return id.length <= 8 ? id : id.slice(0, 8) + "…";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Time ago string. */
|
|
126
|
+
function timeAgo(date: Date): string {
|
|
127
|
+
const ms = Date.now() - date.getTime();
|
|
128
|
+
if (ms < 5_000) return "just now";
|
|
129
|
+
if (ms < 60_000) return `${Math.floor(ms / 1_000)}s ago`;
|
|
130
|
+
if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m ago`;
|
|
131
|
+
if (ms < 86_400_000) return `${Math.floor(ms / 3_600_000)}h ago`;
|
|
132
|
+
return `${Math.floor(ms / 86_400_000)}d ago`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Widget factory ──────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
export type AcpWidgetDeps = {
|
|
138
|
+
getState(): AcpWidgetState;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export type AcpWidgetFactory = (
|
|
142
|
+
tui: unknown,
|
|
143
|
+
theme: Theme,
|
|
144
|
+
) => Component & { dispose?(): void };
|
|
145
|
+
|
|
146
|
+
export function createAcpWidget(deps: AcpWidgetDeps): AcpWidgetFactory {
|
|
147
|
+
return (_tui: unknown, theme: Theme): Component & { dispose?(): void } => {
|
|
148
|
+
// Refresh every 5s to update "time ago" displays
|
|
149
|
+
const refreshInterval = setInterval(() => {
|
|
150
|
+
// No-op; the widget is re-rendered on each TUI paint cycle
|
|
151
|
+
// via the closure over deps.getState()
|
|
152
|
+
}, 5_000);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
render(width: number): string[] {
|
|
156
|
+
const state = deps.getState();
|
|
157
|
+
|
|
158
|
+
// Hide when nothing to show
|
|
159
|
+
const hasWorkers = (state.workers?.length ?? 0) > 0;
|
|
160
|
+
if (
|
|
161
|
+
state.sessions.length === 0 &&
|
|
162
|
+
state.configuredAgentNames.length === 0 &&
|
|
163
|
+
!hasWorkers
|
|
164
|
+
) {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const lines: string[] = [];
|
|
169
|
+
|
|
170
|
+
// ── Header ──
|
|
171
|
+
const header = " " + theme.bold(theme.fg("accent", "ACP Agents"));
|
|
172
|
+
lines.push(truncateToWidth(header, width));
|
|
173
|
+
|
|
174
|
+
// ── Circuit breaker state (only show if not closed) ──
|
|
175
|
+
if (state.circuitBreakerState !== "closed") {
|
|
176
|
+
const cb = CB_ICON[state.circuitBreakerState] ?? CB_ICON["open"];
|
|
177
|
+
const cbLabel =
|
|
178
|
+
state.circuitBreakerState === "half-open"
|
|
179
|
+
? "half-open (probing)"
|
|
180
|
+
: state.circuitBreakerState;
|
|
181
|
+
lines.push(
|
|
182
|
+
truncateToWidth(
|
|
183
|
+
` ${theme.fg(cb.color, `${cb.icon} circuit breaker: ${cbLabel}`)}`,
|
|
184
|
+
width,
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const totalTransient =
|
|
190
|
+
state.activity.activeDelegations +
|
|
191
|
+
state.activity.activeBroadcasts +
|
|
192
|
+
state.activity.activeCompares;
|
|
193
|
+
const activitySummary = state.activity.lastError
|
|
194
|
+
? `error: ${state.activity.lastError}`
|
|
195
|
+
: totalTransient > 1
|
|
196
|
+
? `busy (${totalTransient})`
|
|
197
|
+
: state.activity.activeDelegations > 0
|
|
198
|
+
? "delegating"
|
|
199
|
+
: state.activity.activeBroadcasts > 0
|
|
200
|
+
? "broadcasting"
|
|
201
|
+
: state.activity.activeCompares > 0
|
|
202
|
+
? "comparing"
|
|
203
|
+
: "idle";
|
|
204
|
+
const activityColor: ThemeColor = state.activity.lastError
|
|
205
|
+
? "error"
|
|
206
|
+
: totalTransient > 0
|
|
207
|
+
? "accent"
|
|
208
|
+
: "muted";
|
|
209
|
+
lines.push(
|
|
210
|
+
truncateToWidth(
|
|
211
|
+
` ${theme.fg(activityColor, `status: ${activitySummary}`)}`,
|
|
212
|
+
width,
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// ── Delegation rows (when active) ──
|
|
217
|
+
if (state.activity.delegations && state.activity.delegations.length > 0) {
|
|
218
|
+
for (const del of state.activity.delegations) {
|
|
219
|
+
const phaseIcon = del.phase === "prompting" ? "\u27F3" : del.phase === "done" ? "\u2713" : "\u23F3";
|
|
220
|
+
const delLine = ` ${theme.fg("accent", `${phaseIcon} ${del.agentName}`)} ${theme.fg("dim", del.phase)}`;
|
|
221
|
+
const delText = del.text ? ` ${theme.fg("dim", truncateToWidth(del.text, 40))}` : "";
|
|
222
|
+
lines.push(truncateToWidth(`${delLine}${delText}`, width));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── Recent delegations (from history) ──
|
|
227
|
+
if (state.activity.delegationHistory && state.activity.delegationHistory.length > 0) {
|
|
228
|
+
const recent = state.activity.delegationHistory.slice(-5);
|
|
229
|
+
lines.push(
|
|
230
|
+
truncateToWidth(
|
|
231
|
+
` ${theme.fg("dim", "─ recent ─")}`,
|
|
232
|
+
width,
|
|
233
|
+
),
|
|
234
|
+
);
|
|
235
|
+
for (const entry of recent) {
|
|
236
|
+
const icon = entry.status === "completed" ? "\u2713" : "\u2717";
|
|
237
|
+
const color = entry.status === "completed" ? "success" : "error";
|
|
238
|
+
const errText = entry.error ? ` ${theme.fg("error", truncateToWidth(entry.error, 40))}` : "";
|
|
239
|
+
const histLine = ` ${theme.fg(color, `${icon} ${entry.agentName}`)}${errText}`;
|
|
240
|
+
lines.push(truncateToWidth(histLine, width));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── No sessions ──
|
|
245
|
+
if (state.sessions.length === 0) {
|
|
246
|
+
const agentList = state.configuredAgentNames.join(", ") || "none";
|
|
247
|
+
lines.push(
|
|
248
|
+
truncateToWidth(
|
|
249
|
+
` ${theme.fg("dim", `(no active sessions) agents: ${agentList}`)}`,
|
|
250
|
+
width,
|
|
251
|
+
),
|
|
252
|
+
);
|
|
253
|
+
// Don't return early — workers may still need rendering below
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── Build rows ──
|
|
257
|
+
// Compute column widths
|
|
258
|
+
const nameWidth = Math.max(
|
|
259
|
+
...state.sessions.map((s) => visibleWidth(s.agentName)),
|
|
260
|
+
8, // minimum
|
|
261
|
+
);
|
|
262
|
+
const idWidth = 8; // short session ID
|
|
263
|
+
|
|
264
|
+
for (const session of state.sessions) {
|
|
265
|
+
const icon = theme.fg(
|
|
266
|
+
STATUS_COLOR[session.status],
|
|
267
|
+
STATUS_ICON[session.status],
|
|
268
|
+
);
|
|
269
|
+
const name = theme.bold(padRight(session.agentName, nameWidth));
|
|
270
|
+
const id = theme.fg(
|
|
271
|
+
"dim",
|
|
272
|
+
shortId(session.sessionId).padEnd(idWidth),
|
|
273
|
+
);
|
|
274
|
+
const friendlyName = session.sessionName
|
|
275
|
+
? ` ${theme.fg("accent", session.sessionName)}`
|
|
276
|
+
: "";
|
|
277
|
+
const statusLabel = theme.fg(
|
|
278
|
+
STATUS_COLOR[session.status],
|
|
279
|
+
padRight(session.status, 7),
|
|
280
|
+
);
|
|
281
|
+
const modelLabel = session.model
|
|
282
|
+
? ` ${theme.fg("muted", session.model)}`
|
|
283
|
+
: "";
|
|
284
|
+
const activity = theme.fg("dim", timeAgo(session.lastActivityAt));
|
|
285
|
+
|
|
286
|
+
const row = ` ${icon} ${name}${friendlyName} ${id} ${statusLabel}${modelLabel} · ${activity}`;
|
|
287
|
+
lines.push(truncateToWidth(row, width));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── Worker rows (persistent workers) ──
|
|
291
|
+
if (state.workers && state.workers.length > 0) {
|
|
292
|
+
lines.push(
|
|
293
|
+
truncateToWidth(
|
|
294
|
+
" " + theme.fg("dim", "─ workers ─"),
|
|
295
|
+
width,
|
|
296
|
+
),
|
|
297
|
+
);
|
|
298
|
+
for (const w of state.workers) {
|
|
299
|
+
// Resolve status icon — use stale icon for stale workers, otherwise map by status
|
|
300
|
+
const statusBase = w.status.startsWith("stale") ? "offline" : w.status;
|
|
301
|
+
const wIcon = WORKER_STATUS_ICON[statusBase] ?? { icon: "○", color: "muted" as ThemeColor };
|
|
302
|
+
const statusColor: ThemeColor = w.status.startsWith("stale") ? "warning" : wIcon.color;
|
|
303
|
+
const staleIndicator = w.stale ? theme.fg("warning", " ⚠ stale") : "";
|
|
304
|
+
const taskInfo = w.currentTaskId ? theme.fg("dim", ` · task=${w.currentTaskId}`) : "";
|
|
305
|
+
const workerLine = ` ${theme.fg(statusColor, wIcon.icon)} ${theme.bold(w.name)}: ${theme.fg(statusColor, w.status)} · tok=${formatTokens(w.tokenCountTotal)} · tools=${w.toolCallCount} · ${w.ageSeconds}s ago${taskInfo}${staleIndicator}`;
|
|
306
|
+
lines.push(truncateToWidth(workerLine, width));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── Separator + Summary ──
|
|
311
|
+
lines.push(
|
|
312
|
+
truncateToWidth(
|
|
313
|
+
" " + theme.fg("dim", "─".repeat(Math.max(0, width - 2))),
|
|
314
|
+
width,
|
|
315
|
+
),
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const totalActive = state.sessions.filter(
|
|
319
|
+
(s) => s.status === "active",
|
|
320
|
+
).length;
|
|
321
|
+
const totalIdle = state.sessions.filter(
|
|
322
|
+
(s) => s.status === "idle",
|
|
323
|
+
).length;
|
|
324
|
+
const totalStale = state.sessions.filter(
|
|
325
|
+
(s) => s.status === "stale",
|
|
326
|
+
).length;
|
|
327
|
+
const totalSessions = state.sessions.length;
|
|
328
|
+
|
|
329
|
+
const summaryParts: string[] = [
|
|
330
|
+
`${totalSessions} session${totalSessions !== 1 ? "s" : ""}`,
|
|
331
|
+
];
|
|
332
|
+
if (totalActive > 0) summaryParts.push(`${totalActive} active`);
|
|
333
|
+
if (totalIdle > 0) summaryParts.push(`${totalIdle} idle`);
|
|
334
|
+
if (totalStale > 0) summaryParts.push(`${totalStale} stale`);
|
|
335
|
+
|
|
336
|
+
// Worker summary counts
|
|
337
|
+
const workerCount = (state.workers?.length ?? 0);
|
|
338
|
+
if (workerCount > 0) {
|
|
339
|
+
const wBusy = state.workers!.filter((w) => w.status === "busy").length;
|
|
340
|
+
const wStale = state.workers!.filter((w) => w.stale).length;
|
|
341
|
+
const wIdle = workerCount - wBusy - wStale - state.workers!.filter((w) => w.status === "offline").length;
|
|
342
|
+
const wParts: string[] = [`${workerCount} worker${workerCount !== 1 ? "s" : ""}`];
|
|
343
|
+
if (wBusy > 0) wParts.push(`${wBusy} busy`);
|
|
344
|
+
if (wIdle > 0) wParts.push(`${wIdle} idle`);
|
|
345
|
+
if (wStale > 0) wParts.push(`${wStale} stale`);
|
|
346
|
+
summaryParts.push(wParts.join(" "));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const agentsConfigured = state.configuredAgentNames.length;
|
|
350
|
+
const defaultLabel = state.defaultAgent
|
|
351
|
+
? ` · default: ${state.defaultAgent}`
|
|
352
|
+
: "";
|
|
353
|
+
|
|
354
|
+
lines.push(
|
|
355
|
+
truncateToWidth(
|
|
356
|
+
` ${theme.fg("muted", summaryParts.join(" · "))} ${theme.fg("dim", `${agentsConfigured} agent${agentsConfigured !== 1 ? "s" : ""} configured${defaultLabel}`)}`,
|
|
357
|
+
width,
|
|
358
|
+
),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
// ── Hints ──
|
|
362
|
+
lines.push(
|
|
363
|
+
truncateToWidth(
|
|
364
|
+
theme.fg("dim", " /acp · /acp-config · acp_status · acp_prompt <msg>"),
|
|
365
|
+
width,
|
|
366
|
+
),
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
return lines;
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
invalidate() {},
|
|
373
|
+
|
|
374
|
+
dispose() {
|
|
375
|
+
clearInterval(refreshInterval);
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
};
|
|
379
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-acp-agents — Adapter factory with mode-based routing.
|
|
3
|
+
*
|
|
4
|
+
* Routing priority:
|
|
5
|
+
* 1. mode === 'acpx' → AcpxAdapter (CLI delegation)
|
|
6
|
+
* 2. mode === 'direct' or undefined → name-based routing to dedicated adapters
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AcpAgentAdapter } from "./adapters/base.js";
|
|
10
|
+
import { AcpxAdapter } from "./adapters/acpx.js";
|
|
11
|
+
import { CodexAcpAdapter } from "./adapters/codex.js";
|
|
12
|
+
import { CustomAcpAdapter } from "./adapters/custom.js";
|
|
13
|
+
import { GeminiAcpAdapter } from "./adapters/gemini.js";
|
|
14
|
+
import { OpenCodeAcpAdapter } from "./adapters/opencode.js";
|
|
15
|
+
import type { AcpAgentConfig, AcpConfig } from "./config/types.js";
|
|
16
|
+
|
|
17
|
+
/** Known adapter names that map to dedicated adapter classes */
|
|
18
|
+
const KNOWN_ADAPTERS = new Set(["gemini", "opencode", "codex"]);
|
|
19
|
+
|
|
20
|
+
export function createAdapter(
|
|
21
|
+
agentName: string,
|
|
22
|
+
agentConfig: AcpAgentConfig,
|
|
23
|
+
_globalConfig: AcpConfig,
|
|
24
|
+
cwd?: string,
|
|
25
|
+
adapterOpts?: { onActivity?: (sessionId: string) => void; onSessionUpdate?: (sessionId: string, update: import("@agentclientprotocol/sdk").SessionUpdate) => void },
|
|
26
|
+
): AcpAgentAdapter {
|
|
27
|
+
const sharedOpts = { onActivity: adapterOpts?.onActivity, onSessionUpdate: adapterOpts?.onSessionUpdate };
|
|
28
|
+
|
|
29
|
+
// Mode-based routing: acpx mode always routes to AcpxAdapter
|
|
30
|
+
if (agentConfig.mode === "acpx") {
|
|
31
|
+
return new AcpxAdapter({ config: agentConfig, cwd, agentName, ...sharedOpts }) as unknown as AcpAgentAdapter;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Direct mode (or undefined/default) → name-based routing
|
|
35
|
+
switch (agentName) {
|
|
36
|
+
case "gemini":
|
|
37
|
+
return new GeminiAcpAdapter({ config: agentConfig, cwd, ...sharedOpts });
|
|
38
|
+
case "opencode":
|
|
39
|
+
return new OpenCodeAcpAdapter({ config: agentConfig, cwd, ...sharedOpts });
|
|
40
|
+
case "codex":
|
|
41
|
+
return new CodexAcpAdapter({ config: agentConfig, cwd, ...sharedOpts });
|
|
42
|
+
default:
|
|
43
|
+
return new CustomAcpAdapter({
|
|
44
|
+
config: agentConfig,
|
|
45
|
+
agentName,
|
|
46
|
+
cwd,
|
|
47
|
+
...sharedOpts,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Check if an agent name has a dedicated adapter */
|
|
53
|
+
export function isKnownAdapter(name: string): boolean {
|
|
54
|
+
return KNOWN_ADAPTERS.has(name);
|
|
55
|
+
}
|