Spora의 에이전트 하네스는 PHP로 작성되었습니다. 이는 당연한 선택은 아닙니다. 대부분의 에이전트 도구는 Python을 사용하며, PHP는 사람들이 웹사이트가 필요할 때 손대는 언어이지 모델과 통신하는 시스템에 필요한 언어는 아닙니다.
저는 그럼에도 불구하고 이 방식을 택했습니다. 네 가지 이유가 있는데, 그중 어느 것도 언어 자체에 관한 것은 아니며, 나머지 모든 것이 작동하게 만든 하나의 제약 조건이 있었습니다.
제약 사항: PHP의 기본 작동 방식은 요청-응답(request/response) 주기입니다
PHP를 실행하는 기본 방식은 요청/응답 루프(request/response loop)입니다. PHP-FPM이 프로세스를 시작하고, 하나의 요청을 처리한 후 종료합니다. 이는 웹페이지에는 좋은 모델이지만, 8초 동안 생각하고 토큰 스트리밍을 위해 소켓 연결을 유지해야 하는 에이전트에게는 어색한 방식입니다.
PHP는 실시간 처리가 가능하며, Spora는 적절하다고 판단되는 곳에서 이를 활용합니다. Docker 이미지는 FrankenPHP를 실행하고, 전체 배포 환경은 Mercure 허브(hub)를 추가하며, worker:run --daemon은 장기 실행 PHP 프로세스입니다. 하지만 이 중 어느 것도 기본 방식은 아닙니다. 이는 '파일을 복사하여 올리는' 것과는 다른 배포 방식이며, 공유 호스팅 환경에서는 일부 기능이 아예 제공되지 않을 수도 있습니다.
따라서 Spora는 기본 작동 방식을 거슬러서 구축된 것이 아니라, 그 기본 작동 방식에 맞춰 구축되었습니다. 하나의 틱(tick)은 하나의 LLM 턴(turn)입니다: 상태를 로드하고, 모델을 호출하며, 결정한 내용을 기록하고, 인간의 승인이 다음 단계를 위해 필요한지 여부를 결정합니다. 이 모든 과정이 하나의 요청 내에서 발생하며, 해당 요청이 끝나면 상태는 데이터베이스에 저장됩니다.
이는 스트리밍 UI에는 제한적으로 보일 수 있습니다. 하지만 바로 이것이 에이전트 하네스가 필요로 하는 정확한 형태입니다. 쓰기 권한을 승인하는 것은 빠르지 않습니다. 4분이 걸릴 수도 있고, 밤새 걸릴 수도 있으며, 심지어 사흘이 걸릴 수도 있습니다. 지속적인 상태 전환(durable state transition)으로 끝나는 틱은 그렇게 오래 일시 중단될 수 있는 단계에 적합한 기본 단위입니다: 메모리에 아무것도 존재하지 않으며, 프로세스가 죽어도 아무것도 손실되지 않고, 다음 틱은 마지막이 멈춘 지점부터 정확하게 재개됩니다. 월요일에 도구 호출을 승인하면, 에이전트는 화요일에도 계속 진행될 수 있습니다.
기본 방식을 넘어서는 더 많은 기능이 필요할 때, Spora는 두 축(작업자(worker)를 누가 실행하는지, 그리고 UI가 어떻게 이를 감지하는지)을 따라 세 가지 구성을 제공합니다:
- 풀 배포(Full Deployment) — 서버 데몬이 큐를 비우고, Mercure가 서버 전송 이벤트(server-sent events)를 통해 업데이트를 푸시합니다. Docker와 VPS 환경에서만 인터페이스가 즉각적입니다.
- 클래식 서버(Classical Server) — Mercure 없이 동일한 데몬을 사용하며, 프런트엔드가 대신 5초마다 폴링합니다. 셸 접근이 가능한 일반적인 LAMP 호스팅 환경입니다.
- 제로 설정 공유 호스팅(Zero-Config Shared Host) — 데몬이 없습니다. 브라우저 자체가 워커 역할을 합니다. 해당 브라우저의 탭 전반에 걸쳐 사용자가 로그인한 자신의 작업을 2초마다 실행합니다. 파일을 업로드하고, 문서 루트를
public/으로 지정한 후 로그인하기만 하면 됩니다.
세 번째 방식은 $5 호스팅 비용이라는 정직한 대가를 지닙니다. 데몬이 없기 때문에 예약된 작업은 브라우저가 열려 있을 때만 전송됩니다. 이것이 FTP 기능만을 필요로 하는 사용자에게 요구되는 것입니다.
PHP로 하네스(harness)를 구축해야 하는 네 가지 이유
사용자가 있는 곳에 호스팅합니다
자체 AI 인프라를 실행하려는 사람은 보통 개발자이거나 공유 cPanel 계정, 저렴한 VPS, 또는 Raspberry Pi를 사용하는 2인 팀입니다. Kubernetes 예산이 있는 플랫폼 팀은 아닙니다.
Spora-core는 PHP 8.4.1과 표준 확장 기능만 필요합니다. 목록 전체가 그것입니다. 설치는 composer create-project spora-ai/spora를 수행한 다음, php bin/spora spora:install을 실행하면 됩니다. SQLite가 기본 데이터베이스이며 MySQL이 프로덕션 목표입니다. Redis도 없고, 브로커도 없고, Node 빌드 단계도 없고, 오케스트레이터도 없습니다. WordPress 사이트를 설정할 수 있는 사람이라면 누구나 Spora를 실행할 수 있습니다.
이는 명백한 대안들의 배포 환경과 비교해 볼 때 더욱 그렇습니다. Python 서비스는 virtualenv, 프로세스 슈퍼바이저(process supervisor), 리버스 프록시(reverse proxy), systemd 유닛을 의미합니다. 백엔드 팀에게는 괜찮지만, 이 사용자들에게는 치명적인 단점입니다. Go나 Rust 서비스는 빌드 파이프라인과 호스트의 libc와 일치해야 하는 바이너리를 의미합니다. PHP 파일은 그저 파일일 뿐입니다. FTP로 업로드하면 실행됩니다.
시스템이 실행되는 동안 플러그인 설치
Spora 플러그인은 plugin.json 매니페스트를 가진 일반적인 Composer 패키지입니다. 설치 프로그램은 이를 plugins/<name>/ 경로로 라우팅하며, Composer는 다른 모든 패키지와 마찬가지로 종속성을 해결합니다. 따라서 HTTP 클라이언트나 PDF 파서가 필요한 플러그인은 단순히 해당 라이브러리를 요구하기만 하면 되고, 솔버(solver)가 나머지 작업을 처리합니다.
이것은 오직 Composer만을 사용하므로, 설치를 위해 재배포할 필요가 없습니다. CLI에서 php bin/spora plugin:install vendor/name 명령을 실행하면 되며, SPORA_PLUGIN_INSTALL_ENABLED 플래그를 켜면 관리자 UI에서 Packagist 카탈로그를 탐색하고 실행 중인 시스템에 대해 플러그인을 설치, 업데이트 또는 제거할 수 있습니다.
이 기능은 선택적(opt-in)인 이유가 있습니다. 설치 프로그램은 외부 명령으로 Composer 바이너리를 실행하며, 공개된 Docker 이미지는 이를 포함하지 않습니다. 따라서 Composer 바이너리가 존재하는 곳에서만 이 플래그를 켜고, 그렇지 않은 곳에서는 꺼두어야 합니다.
모델이 언어를 이미 알고 있다
제가 예상치 못한 주장을 하나 하겠습니다. 알려진 서버 측 언어를 사용하는 웹사이트 중 69.2%가 PHP를 사용하며, 그 대다수가 이미 버전 8을 실행하고 있습니다 (W3Techs, 2026년 10월). 이는 엄청난 양의 공개된 PHP를 의미하며, 모델이 코드를 학습하는 데 있어 이 언어군에서 가장 많이 본 것이 바로 공개된 PHP입니다.
이는 출력물에서도 나타납니다. 모델에게 PHP 클래스를 요청하면 해당 생태계와 일치하는 규칙(convention)을 생성합니다: PSR-4 네임스페이스 지정, 올바른 키가 포함된 Composer 매니페스트, Symfony 스타일의 의존성 주입(dependency injection), 직접 구현한 리포지토리 대신 Eloquent 모델 등입니다. 누군가 지시했기 때문이 아니라—그것이 학습 데이터의 모습이기 때문입니다.
만약 에이전트가 코드를 작성하거나 플러그인을 생성한다면, 가장 잘 작성하는 언어는 코퍼스(corpus)에 가장 많은 코드가 있는 언어일 것입니다. PHP가 에이전트 도구(tooling)를 구축하는 데 사용되는 언어는 아닐지라도, 모델들이 놀라울 정도로 유창하게 작성하는 언어입니다.
통합 생태계가 이미 해결되었다
Packagist는 30만 개가 넘는 패키지를 나열하고 있습니다. 에이전트 하네스가 필요로 하는 통합 기능들—결제, 메일, 스토리지, OAuth, 번역, PDF, 이미지 처리 등—은 연구 프로젝트가 아닙니다. 이들은 composer require 명령 한 번으로 해결할 수 있으며, 이미 버전 관리가 되어 있고, 유지보수가 되고 있으며, 현재 사용하고 계신 PHP 버전과도 호환됩니다.
Spora-core는 정확히 이러한 점에 의존합니다: API 통신을 위한 symfony/http-client, 메일을 위한 symfony/mailer, 데이터베이스를 위한 illuminate/database, 라우팅을 위한 nikic/fast-route, 컨테이너를 위한 php-di/php-di, Markdown 처리를 위한 league/commonmark입니다. 이 모든 것은 해당 문제를 전업으로 다루는 사람들에 의해 해결된 문제입니다. SMTP 클라이언트가 필요한 하네스는 직접 작성해서는 안 됩니다.
이것이 플러그인 개발자에게 언어 장벽이 없는 이유이기도 합니다. PHP 클래스를 작성할 수 있다면, Spora 플러그인을 작성할 수 있습니다.
제가 포기한 것들
- 단일 턴 내 동시성(Concurrency). PHP의 요청/응답 모델은 이벤트 루프(event loop)가 없기 때문에, 하나의 LLM 턴 내부에서 실행되는 도구 호출들은 나란히(side by side) 실행되기보다는 순차적으로(in sequence) 실행됩니다. 에이전트는 이미 병렬로 실행되지만, 현재는 턴 내의 분산 처리(fan-out within a turn)가 순차적입니다. PHP는 8.1 버전부터 네이티브 Fibers를 지원했으며, Swoole은 단 하나의 확장만 거리가 먼—대부분의 공유 호스팅 환경에서는 제공하지 않는—도구입니다. 병렬 도구 호출 기능은 로드맵에 있습니다. 먼저 단순한 버전을 출시하는 것은 의도적인 결정입니다. 첫 번째 릴리스의 목표는 개발자가 이를 설치하고 에이전트가 작동하는 것을 보는 것이지, 가장 빠르게 벤치마크 하는 것이 아닙니다.
- 런타임 플러그인 설치를 위해서는 쓰기 가능한 디스크와 Composer 바이너리가 필요합니다. 이 두 가지 모두 일반적인 요소이며, 공유 호스팅 환경에서는 간혹 누락되는 경우가 있습니다.
- AI 생태계는 개발자들을 Python과 TypeScript 쪽으로 끌어당깁니다. PHP는 가장 큰 서버 측 언어 중 하나이므로 풀(pool)의 크기가 문제는 아닙니다. 하지만 AI 도구를 구축하려는 그 풀의 일부가 PHP라서 더 커지는 것은 아닙니다. 에이전트 작업은 사람들을 Python 및 TypeScript 쪽에서 끌어오며, 이들은 기본적으로 PHP로 오지 않습니다.
제가 포기할 것이라 예상했던 것들
**스트리밍(Streaming)**은 제가 잃을 것이라고 예상했던 부분입니다. 작업 업데이트는 이미 서버 전송 이벤트(server-sent events, SSE)를 통해 푸시되고 있으며, 티크 모델(tick model) 역시 더 많은 데이터를 푸시하는 것을 방해하지 않습니다. 모델 자체의 출력을 토큰 단위로 스트리밍하는 것이 다음 이정표입니다. 이를 통해 드라이버는 선택적 스트리밍 모드를 갖게 되며, Mercure는 이미 이를 전달할 SSE 파이프라인을 제공합니다. 이는 새로운 서브시스템이라기보다는 기존에 존재하는 것에 추가되는 단계입니다.
**도구(Tooling)**가 또 다른 부분입니다. 서버 측 작업을 할 때 Python은 PHP보다 10년 앞서 나간 것이 아닙니다. PHPStan과 Larastan은 실제 버그를 찾아내고, Pest는 최고 수준의 테스트 프레임워크이며, Rector와 PHP-CS-Fixer는 기계적인 편집을 담당하고, FrankenPHP는 현대적인 애플리케이션 서버입니다. 남겨진 격차는 데이터 스택(data stack) — 노트북, NumPy, 과학 라이브러리 — 이고, 오케스트레이션 레이어(orchestration layer)가 이를 활용할 필요가 없습니다.
제가 사용한 규칙
배포 현실이 사용자들의 배포 현실과 일치하는 언어를 선택하십시오. Spora의 사용자는 저렴한 호스트에서 자체 인프라를 운영하며, 플러그인을 통해 스스로 확장합니다. PHP는 호스트, 사용자, 런타임이 이미 '배포'가 무엇을 의미하는지에 대해 합의하고 있는 유일한 언어입니다.
두 번째 규칙은 덜 명확합니다: 해당 언어의 기본 동작(default)은 제품이 필요로 하지 않는 것이어야 합니다. PHP의 기본 동작은 요청이 끝나고 프로세스가 사라지는 것입니다. 사람이 쓰기를 승인하기를 기다리는 에이전트 하네스에게는 정확히 이것이 필요합니다. 즉, 단계가 끝나고 상태가 영구적으로 저장되며(durable), 모델이 마음을 바꿀 때까지 아무것도 열려있지 않아야 합니다.
실제에서도 이 규칙이 유효한지 확인하고 싶다면, Spora를 설치하여 직접 테스트해 보십시오. 만약 플러그인부터 시작하는 것이 더 좋다면, 플러그인 스켈레톤은 약 80줄의 PHP 코드입니다.