Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/dockerhub-branch-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,20 @@ jobs:
include:
- name: korokd
image: agentland-korokd
context: .
dockerfile: docker/Dockerfile.korokd
- name: gateway
image: agentland-gateway
context: .
dockerfile: docker/Dockerfile.gateway
- name: agentcore
image: agentland-agentcore
context: .
dockerfile: docker/Dockerfile.agentcore
- name: agent
image: agentland-agent
context: app/agentland-agent
dockerfile: app/agentland-agent/Dockerfile

steps:
- uses: actions/checkout@v4
Expand All @@ -40,7 +47,7 @@ jobs:

- uses: docker/build-push-action@v6
with:
context: .
context: ${{ matrix.context }}
file: ${{ matrix.dockerfile }}
push: true
tags: ${{ env.DOCKERHUB_NAMESPACE }}/${{ matrix.image }}:latest
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ help: ## Display this help.

.PHONY: manifests
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
$(CONTROLLER_GEN) rbac:roleName=manager-role crd:maxDescLen=0 webhook paths="./..." output:crd:artifacts:config=config/crd/bases

.PHONY: sync-chart-crds
sync-chart-crds: manifests ## Sync generated CRDs into the Helm chart.
Expand Down
17 changes: 13 additions & 4 deletions api/v1alpha1/sandbox_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,30 @@ limitations under the License.
package v1alpha1

import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// SandboxTemplate defines a generic pod startup template for all sandbox types.
type SandboxTemplate struct {
// +kubebuilder:validation:Required
Image string `json:"image"`
// RuntimeClassName controls which container runtime to use for the sandbox Pod
// Typical values include "kata-qemu" or "gvisor" depending on cluster RuntimeClass setup
// Image is the legacy single-container shortcut. It is ignored when PodSpec is set.
// +optional
Image string `json:"image,omitempty"`
// RuntimeClassName controls which container runtime to use for the sandbox Pod.
// It is ignored when PodSpec.runtimeClassName is set.
// Typical values include "kata-qemu" or "gvisor" depending on cluster RuntimeClass setup.
// +optional
RuntimeClassName string `json:"runtimeClassName,omitempty"`
// Command is the legacy single-container shortcut. It is ignored when PodSpec is set.
// +optional
Command []string `json:"command,omitempty"`
// Args is the legacy single-container shortcut. It is ignored when PodSpec is set.
// +optional
Args []string `json:"args,omitempty"`
// PodSpec is the official Kubernetes PodSpec used to create sandbox Pods.
// Agentland still injects its reserved JWT and workspace volumes into the resulting Pod.
// +optional
PodSpec *corev1.PodSpec `json:"podSpec,omitempty"`
}

// SandboxSpec defines the desired state of Sandbox.
Expand Down
6 changes: 6 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions app/agentland-agent/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__/
scripts/
.venv/
45 changes: 45 additions & 0 deletions app/agentland-agent/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
FROM ubuntu:22.04

ENV DEBIAN_FRONTEND=noninteractive \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
NPM_CONFIG_REGISTRY=https://registry.npmmirror.com \
VIRTUAL_ENV=/opt/venv \
PATH=/opt/venv/bin:$PATH

WORKDIR /app

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
curl \
git \
gnupg \
software-properties-common \
&& install -d -m 0755 /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
| gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" \
> /etc/apt/sources.list.d/nodesource.list \
&& add-apt-repository -y ppa:deadsnakes/ppa \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
nodejs \
python3.12 \
python3.12-venv \
&& python3.12 -m venv /opt/venv \
&& pip install --upgrade pip setuptools wheel \
&& npm config set registry "${NPM_CONFIG_REGISTRY}" \
&& apt-get purge -y --auto-remove software-properties-common \
&& rm -rf /var/lib/apt/lists/*

COPY requirements.txt .

RUN pip install -r requirements.txt

COPY app ./app

EXPOSE 8000

CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
128 changes: 128 additions & 0 deletions app/agentland-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Agentland Agent FastAPI demo

这个 demo 提供一个纯 HTTP 的 LangGraph coding agent 服务。它现在有两层
能力:

- 一个普通 agent 分支,用于持续对话
- 一个 Ralph 分支,用于执行多轮任务编排

统一入口会先经过一个 LangGraph graph router。router agent 会输出结构化
JSON,将请求分流到 chat 或 task 分支。

项目结构如下:

- `app/main.py`:应用入口
- `app/api/endpoints/`:HTTP 路由
- `app/schemas/`:请求模型
- `app/models/`:运行态模型
- `app/services/`:核心逻辑,包括 agent loop、router、memory、Ralph
- `app/database/`:数据库占位目录
- `tests/`:测试

## Start the service

按下面的步骤启动服务:

```bash
cd demo/agentland-agent
uv pip install -r requirements.txt
export OPENAI_API_KEY=your_key
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000
```

## API overview

服务暴露以下接口:

- `POST /v1/chat/stream`:统一 SSE chat 接口,内部先走 graph router
- `POST /v1/ralph/stream`:直接启动 Ralph 风格的 10 轮外层编排
- `POST /v1/sessions/{session_id}/steer`:向活跃 chat 会话写入 steering
消息
- `POST /v1/sessions/{session_id}/followup`:向活跃 chat 会话写入
follow-up 消息
- `GET /health`:健康检查

## Unified chat interface

`POST /v1/chat/stream` 是整合后的入口。它先调用一个 LangGraph router
node。router 会返回 JSON 结果:

```json
{
"intent": "chat",
"reason": "plain conversation",
"source": "model"
}
```

然后 graph 会按 `intent` 分支:

- `chat`:走普通 agent 服务
- `task`:走 Ralph loop 服务

返回仍然是 `text/event-stream`。这个接口会先发送一条 `route` 事件,然后
继续发送目标分支自己的 SSE 事件。

请求体示例:

```json
{
"message": "请帮我实现一个需求,并修改工作区里的代码",
"workspace_path": "/absolute/path/to/workspace",
"session_id": "demo-session"
}
```

当它进入普通 chat 分支时,会先发送一条 `session` 事件,其中包含
`session_file`。这个文件使用和 `pi-mono` 相同原理的 append-only JSONL
memory。

## Persistent memory

普通 chat 分支不再依赖进程内 `history`。它使用一个持久化 session manager:

- 会话目录按 `cwd` 分桶
- 会话文件是 append-only JSONL
- 每条 entry 都有 `id` 和 `parentId`
- 当前上下文由 leaf 路径回放得到
- 支持 `compaction`、`branch_summary`、`custom`、`custom_message`

默认会话根目录是 `~/.pi/agent/sessions/`。测试时你也可以通过
`PI_SESSION_ROOT` 覆盖它。

## Ralph interface

`POST /v1/ralph/stream` 直接接收一个原始需求,并在现有 `run_agent`
之上启动 Ralph 风格的外层循环。

它和参考 `ralph` 保持同一原理:

- 外层循环默认最多运行 10 轮
- 每一轮都以全新的聊天上下文重新启动 agent
- 持久化状态落到工作区 `.ralph/<session_id>/prd.json` 和
`.ralph/<session_id>/progress.txt`
- 只有当 agent 输出 `<promise>COMPLETE</promise>` 时才提前停止

请求体示例:

```json
{
"requirement": "为当前项目增加一个 Ralph 风格的任务编排接口",
"workspace_path": "/absolute/path/to/workspace",
"session_id": "ralph-demo"
}
```

除了常规的 `assistant_delta`、`tool_call`、`tool_result` 事件以外,
Ralph 还会发送这些生命周期事件:

- `session`
- `planner_fallback`
- `plan_ready`
- `iteration_start`
- `iteration_complete`
- `done`

为了降低 OpenAI 兼容网关差异带来的影响,chat router、普通 agent 和
Ralph planner 都显式关闭了 `Responses API` 自动路由,优先使用更通用的
chat completions 路径。
2 changes: 2 additions & 0 deletions app/agentland-agent/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"""Agentland Agent FastAPI 应用包。"""

14 changes: 14 additions & 0 deletions app/agentland-agent/app/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""API 路由聚合。"""

from fastapi import APIRouter

from app.api.endpoints.chat import router as chat_router
from app.api.endpoints.health import router as health_router
from app.api.endpoints.ralph import router as ralph_router
from app.api.endpoints.sessions import router as sessions_router

api_router = APIRouter()
api_router.include_router(health_router)
api_router.include_router(chat_router)
api_router.include_router(ralph_router)
api_router.include_router(sessions_router)
2 changes: 2 additions & 0 deletions app/agentland-agent/app/api/endpoints/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"""API 端点模块。"""

19 changes: 19 additions & 0 deletions app/agentland-agent/app/api/endpoints/chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from __future__ import annotations

"""聊天流式端点。"""

from fastapi import APIRouter
from fastapi.responses import StreamingResponse

from app.schemas.chat import ChatStreamRequest
from app.services.chat_service import stream_chat

router = APIRouter()


@router.post("/v1/chat/stream")
async def chat_stream(request: ChatStreamRequest) -> StreamingResponse:
"""启动一次会话并以 SSE 流式返回事件。"""

return await stream_chat(request)

15 changes: 15 additions & 0 deletions app/agentland-agent/app/api/endpoints/health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from __future__ import annotations

"""健康检查端点。"""

from fastapi import APIRouter

router = APIRouter()


@router.get("/health")
async def health() -> dict[str, str]:
"""返回服务健康状态。"""

return {"status": "ok"}

18 changes: 18 additions & 0 deletions app/agentland-agent/app/api/endpoints/ralph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from __future__ import annotations

"""Ralph-compatible stream endpoint."""

from fastapi import APIRouter
from fastapi.responses import StreamingResponse

from app.schemas.ralph import RalphStreamRequest
from app.services.ralph_service import stream_ralph

router = APIRouter()


@router.post("/v1/ralph/stream")
async def ralph_stream(request: RalphStreamRequest) -> StreamingResponse:
"""Start or resume a Ralph-style orchestration loop."""

return await stream_ralph(request)
25 changes: 25 additions & 0 deletions app/agentland-agent/app/api/endpoints/sessions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import annotations

"""会话控制端点(steer/follow-up)。"""

from fastapi import APIRouter

from app.schemas.chat import QueueMessageRequest
from app.services.chat_service import queue_followup, queue_steering

router = APIRouter()


@router.post("/v1/sessions/{session_id}/steer")
async def steer(session_id: str, request: QueueMessageRequest) -> dict[str, object]:
"""向会话的 steering 队列写入消息。"""

return queue_steering(session_id=session_id, message=request.message)


@router.post("/v1/sessions/{session_id}/followup")
async def followup(session_id: str, request: QueueMessageRequest) -> dict[str, object]:
"""向会话的 follow-up 队列写入消息。"""

return queue_followup(session_id=session_id, message=request.message)

2 changes: 2 additions & 0 deletions app/agentland-agent/app/database/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"""数据库层占位目录(当前 demo 使用内存会话,无外部数据库)。"""

19 changes: 19 additions & 0 deletions app/agentland-agent/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from __future__ import annotations

"""FastAPI 应用主入口。"""

from fastapi import FastAPI

from app.api import api_router


def create_app() -> FastAPI:
"""创建并装配 FastAPI 应用。"""

application = FastAPI(title="LangGraph Coding Agent SSE Service")
application.include_router(api_router)
return application


app = create_app()

Loading
Loading