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
30 changes: 30 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
bin/

# Test binary
*.test

# Output of the go coverage tool
*.out

# Python
__pycache__/
*.py[cod]

# IDE
.idea/
.vscode/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Local Docs (Do not commit)
/docs/
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# linkwork-executor

English | [中文](./README_zh-CN.md)

`linkwork-executor` is the LinkWork task worker. It consumes tasks from Redis queues, prepares workspace context, invokes `AgentEngine`, handles interrupts/archive, and performs lifecycle scale-down on idle timeout.

## Entry Points

- Python package entry: `linkwork_executor.Worker`
- CLI entry: `linkwork-executor-worker`
- Default config path: `/opt/agent/config.json`

## Local Development

### 1) Requirements

- Python 3.11+
- Redis

### 2) Install

```bash
cd linkwork-agent-sdk && pip install -e .
cd ../linkwork-executor && pip install -e .
```

### 3) Start worker

```bash
cd linkwork-executor
WORKSTATION_ID=ws-demo \
REDIS_URL=redis://127.0.0.1:6379 \
linkwork-executor-worker --config ./config.json
```

Required env var:

- `WORKSTATION_ID`

Common env vars:

- `REDIS_URL` (default: `redis://redis:6379`)
- `IDLE_TIMEOUT`
- `TASK_RUNTIME_IDLE_TIMEOUT`
- `WORKER_DESTROY_API_BASE`
- `WORKER_DESTROY_API_PASSWORD`

## Deploy Flow

### Option A: Run inside role image (primary path)

In `LinkWork/back`, build flow copies `linkwork-executor` source into role images. Runtime is started through:

- `start-single.sh` (single-container mode)
- `start-dual.sh` (agent + runner mode)

These scripts manage permission setup, `zzd` startup, worker startup, and graceful shutdown.

### Option B: Publish as standalone package (optional)

```bash
cd linkwork-executor
python -m build
# twine upload dist/* # use your internal release process
```

## Related Components

- Depends on `linkwork-agent-sdk`
- Upstream scheduler: `LinkWork/back`
- Data channel: Redis queue + Redis stream
69 changes: 69 additions & 0 deletions README_zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# linkwork-executor

`linkwork-executor` 是 LinkWork 任务执行器,负责从 Redis 队列消费任务、准备工作区、调用 `AgentEngine`、处理中断与归档,并在空闲超时后执行生命周期回收。

## 核心入口

- Python 包入口:`linkwork_executor.Worker`
- CLI 入口:`linkwork-executor-worker`
- 默认配置路径:`/opt/agent/config.json`

## 本地开发

### 1) 环境要求

- Python 3.11+
- Redis

### 2) 安装

```bash
cd linkwork-agent-sdk && pip install -e .
cd ../linkwork-executor && pip install -e .
```

### 3) 启动 Worker

```bash
cd linkwork-executor
WORKSTATION_ID=ws-demo \
REDIS_URL=redis://127.0.0.1:6379 \
linkwork-executor-worker --config ./config.json
```

必需环境变量:

- `WORKSTATION_ID`

常用环境变量:

- `REDIS_URL`(默认 `redis://redis:6379`)
- `IDLE_TIMEOUT`
- `TASK_RUNTIME_IDLE_TIMEOUT`
- `WORKER_DESTROY_API_BASE`
- `WORKER_DESTROY_API_PASSWORD`

## Deploy 流程

### 方案 A:随角色镜像运行(主路径)

`LinkWork/back` 构建阶段会把 `linkwork-executor` 源码复制到镜像中;运行阶段由以下脚本拉起:

- 单容器:`start-single.sh`
- 双容器(Agent + Runner):`start-dual.sh`

脚本会完成权限初始化、`zzd` 启动、Worker 启动与优雅退出管理。

### 方案 B:独立打包发布(可选)

```bash
cd linkwork-executor
python -m build
# twine upload dist/* # 按内部流程发布
```

## 与其他模块关系

- 依赖:`linkwork-agent-sdk`
- 上游调度:`LinkWork/back`(任务下发与状态管理)
- 数据通道:Redis 队列 + Redis Stream
31 changes: 31 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[project]
name = "linkwork-executor"
version = "0.1.0"
description = "LinkWork Executor"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"linkwork-agent-sdk>=0.1.0",
]

[project.scripts]
linkwork-executor-worker = "linkwork_executor.work.worker:main"

[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.23",
]

[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages.find]
where = ["src"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
python_files = ["test_*.py"]

5 changes: 5 additions & 0 deletions src/linkwork_executor/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""LinkWork Executor package."""

from .work.worker import Worker

__all__ = ["Worker"]
16 changes: 16 additions & 0 deletions src/linkwork_executor/work/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Work layer package."""

from .consumer import GitRepoConfig, Task, TaskConsumer
from .lifecycle import LifecycleManager
from .workspace import TaskStatus, WorkspaceManager
from .worker import Worker

__all__ = [
"GitRepoConfig",
"LifecycleManager",
"Task",
"TaskConsumer",
"TaskStatus",
"Worker",
"WorkspaceManager",
]
55 changes: 55 additions & 0 deletions src/linkwork_executor/work/agents_guide.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Workspace AGENTS.md helper utilities."""

from __future__ import annotations

from pathlib import Path

AGENTS_FILE_PATH = Path("/workspace/AGENTS.md")
AGENTS_DEFAULT_HEADER = "# AGENTS Workspace Guide"
SKILL_GUIDANCE_HEADER = "## Skill 使用规范(Claude Native)"
SKILL_GUIDANCE_BLOCK = """## Skill 使用规范(Claude Native)

1. Skill 是“流程指引”,MCP 是“外部能力”。
- 仅加载 Skill 不代表具备外部检索能力。
- 若 Skill 依赖 Tavily,必须确保已绑定并加载 Tavily MCP。

2. loaded != referenced。
- `SKILLS_LOADED` 仅表示已加载。
- 只有实际调用 `Skill` 工具或读取 Skill 文件,才算真正引用。

3. 禁止 slash 命令话术。
- 不输出 `/commit`、`/review-pr` 等样式。
- 统一使用自然语言和结构化结论表达。

4. 大结果检索必须走分批流程。
- Tavily 多查询结果先落地 `/tmp/*.json`。
- 再做“去重压缩 → 分批小结 → 最终汇总”,避免上下文或超时问题。

5. 产物规范。
- `/tmp` 仅临时文件,不作为最终交付。
- 最终文件必须写入 `/workspace/workstation`。

6. 能力降级时必须显式说明。
- 若 MCP 不可用或证据不足,明确“待确认项”和下一步动作,禁止编造结论。"""


def ensure_workspace_agents_skill_guidance(path: Path = AGENTS_FILE_PATH) -> None:
"""Ensure AGENTS.md contains skill guidance section."""
path.parent.mkdir(parents=True, exist_ok=True)
existing = path.read_text(encoding="utf-8") if path.exists() else ""
updated = upsert_workspace_agents_skill_guidance(existing)
if updated != existing:
path.write_text(updated, encoding="utf-8")


def upsert_workspace_agents_skill_guidance(content: str) -> str:
"""Append skill guidance section when missing."""
normalized = content.replace("\r\n", "\n")
if SKILL_GUIDANCE_HEADER in normalized:
return normalized

prefix = normalized.strip()
if not prefix:
return f"{AGENTS_DEFAULT_HEADER}\n\n{SKILL_GUIDANCE_BLOCK}\n"
return f"{prefix}\n\n{SKILL_GUIDANCE_BLOCK}\n"

Loading