Skip to content
Merged
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
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ loopd/
可扩展 blocks 承载文本、可见工具状态和产物,不混入完整执行轨迹。
3. `conversations` 与 `messages` 均使用 go-stdx 生成的 UUIDv7 `id` 作为主键和游标;不再维护平行的
message sequence。
4. 一次问答由 user Message、responder Message 和同 ID 的 Task CRD 组成。服务端在数据库事务提交前
创建 CRD;创建失败则回滚两条 Message 并向 Human 返回错误
5. 主会话的 `parent_message_id` 为空;Operator 内部工作会话通过该字段引用主链路 responder Message。
4. 一次问答由 user Message、目标 Actor 的 response Message 和同 ID 的 Task CRD 组成。服务端在数据库事务提交前
初始化 AgentUE Redis Stream 并创建 CRD;任一步失败则回滚两条 Message,并尽力删除已创建的外部资源
5. 主会话的 `parent_message_id` 为空;Operator 内部工作会话通过该字段引用主链路 response Message。
v1 不允许详情会话继续嵌套,且同一条 Message 最多关联一个详情会话。
6. v1alpha1 Task CRD 当前保存路由和唤醒信息,详细上下文由 runtime 按 Task ID 向 server 查询。公共协调
字段可按 Kubernetes API 兼容规则增量演进;领域状态复杂时,Operator 创建并拥有自己的 CRD。
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Loop = Resource(spec + status) + Reconcile
```

Every chat request creates a small loopd Task CRD that wakes the selected
responder. A simple Operator can reconcile that Task directly; a complex
Actor. A simple Operator can reconcile that Task directly; a complex
Operator may create domain CRDs for its own state and completion semantics.
`loopd` keeps visible collaboration in conversations and messages, while full
execution history belongs to the Harness and AgentLedger.
Expand All @@ -26,7 +26,7 @@ execution history belongs to the Harness and AgentLedger.
- **Harness adapters** connect loop-server to agentd or another intelligent
execution service without leaking provider vocabulary into the public model.

AgentUE is used for the page-visible message model. AgentLedger records complete
AgentUE supplies the page-visible event model and Redis bridge. AgentLedger records complete
prompts, model events, tool calls, retries, and costs; it is not the chat database.

The public conversation roles are always:
Expand All @@ -40,15 +40,15 @@ user | harness | operator
A question may run for minutes or days. Its lifecycle is independent from any
HTTP request or browser connection:

1. loop-server creates a user message and an empty responder message with one
`task_id`, then creates a same-ID Task CRD before committing the messages;
1. loop-server creates a user message and an empty target response message with one
`task_id`, initializes its AgentUE stream, then creates a same-ID Task CRD before committing the messages;
2. the selected Operator or Harness watches the Task and resolves its current
input and conversation history through loop-runtime;
3. a complex Operator may create a domain CRD, while a simple Operator handles
the shared Task directly;
4. visible progress updates the responder message while full events enter AgentLedger;
5. clients may disconnect and reload the current message snapshot;
6. the selected Harness or Operator finishes the same responder message.
4. visible progress flows through the AgentUE Redis event bridge while full events enter AgentLedger;
5. clients may reconnect to any server instance with the same `task_id` and continue from their last event cursor;
6. completion folds visible events into the selected Actor's response Message.

`Harness.Prompt` returns a handle. A Reconciler can inspect it and return, or
call `Wait` when waiting is genuinely the only remaining work. Both paths use
Expand Down
6 changes: 3 additions & 3 deletions chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type Invocation struct {
ConversationID string `json:"conversation_id"`
InputMessageID string `json:"input_message_id"`
OutputMessageID string `json:"output_message_id,omitempty"`
Responder ResponderRef `json:"responder"`
Target ActorRef `json:"target"`
ContextThroughMessageID string `json:"context_through_message_id"`
Phase InvocationPhase `json:"phase"`
Resource *ResourceRef `json:"resource,omitempty"`
Expand Down Expand Up @@ -72,7 +72,7 @@ type Activity struct {
InvocationID string `json:"invocation_id"`
Key string `json:"key"`
ParentID string `json:"parent_id,omitempty"`
Actor ResponderRef `json:"actor"`
Actor ActorRef `json:"actor"`
Kind string `json:"kind"`
Title string `json:"title"`
Detail string `json:"detail,omitempty"`
Expand All @@ -97,7 +97,7 @@ type OperatorEvent struct {
type ActivityUpdate struct {
Key string `json:"key"`
ParentID string `json:"parent_id,omitempty"`
Actor ResponderRef `json:"actor"`
Actor ActorRef `json:"actor"`
Kind string `json:"kind"`
Title string `json:"title"`
Detail string `json:"detail,omitempty"`
Expand Down
10 changes: 9 additions & 1 deletion cmd/loop-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,20 @@ func main() {
func run() error {
address := envOr("LOOP_SERVER_ADDR", ":8080")
databasePath := envOr("LOOP_SERVER_DB", "loopd.db")
redisAddress := envOr("LOOP_SERVER_REDIS_ADDR", "127.0.0.1:6379")
taskNamespace := envOr("LOOP_SERVER_TASK_NAMESPACE", "default")
tasks, err := newTaskClient(taskNamespace)
if err != nil {
return err
}
loopServer, err := server.New(server.Config{
Database: server.DatabaseConfig{Path: databasePath}, Tasks: tasks, Logger: slog.Default(),
Database: server.DatabaseConfig{Path: databasePath},
Redis: server.RedisConfig{
Address: redisAddress,
Username: os.Getenv("LOOP_SERVER_REDIS_USERNAME"),
Password: os.Getenv("LOOP_SERVER_REDIS_PASSWORD"),
},
Tasks: tasks, Logger: slog.Default(),
})
if err != nil {
return err
Expand Down Expand Up @@ -63,6 +70,7 @@ func run() error {
logger.Info("loop-server listening",
"address", address,
"database", databasePath,
"redis_address", redisAddress,
"task_namespace", taskNamespace,
)
serveErr <- httpServer.Run()
Expand Down
12 changes: 8 additions & 4 deletions common.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,21 @@ func (role Role) Valid() bool {
}
}

type ResponderRef struct {
type ActorRef struct {
Kind Role `json:"kind"`
Key string `json:"key"`
}

func (ref ResponderRef) Valid() bool {
func (ref ActorRef) Valid() bool {
return ref.Kind.Valid() && ref.Key != ""
}

func (ref ActorRef) ValidTarget() bool {
return (ref.Kind == RoleHarness || ref.Kind == RoleOperator) && ref.Key != ""
}

type Responder struct {
ResponderRef
type Actor struct {
ActorRef
DisplayName string `json:"display_name,omitempty"`
Description string `json:"description,omitempty"`
}
Expand Down
43 changes: 27 additions & 16 deletions docs/kernel.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ loopd 的定位分为三个维度:
一句话概括:

> Human 在持久 Conversation 中向 Harness 或 Operator 提出目标;loop-server 创建 Task CRD 唤醒
> responder,Operator 按需建立领域 CRD、调用 Harness,并把过程与结果带回同一 Conversation。
> 选定的 Actor,Operator 按需建立领域 CRD、调用 Harness,并把过程与结果带回同一 Conversation。

loopd 在四层 Agent 技术体系中的位置如下:

Expand All @@ -37,7 +37,7 @@ loopd 的公开协作角色只有三类:

| 参与者 | 职责 |
|---|---|
| **Human** | 提出目标、选择 responder、补充上下文、回答 Interaction,并判断结果是否满足需要 |
| **Human** | 提出目标、选择 Actor、补充上下文、回答 Interaction,并判断结果是否满足需要 |
| **Harness** | 接受 prompt 与 tools,执行一次可流式观察的智能任务;既可直接回答 Human,也可被 Operator 调用 |
| **Operator** | Reconcile loopd Task,读取外部事实并决定何时调用 Harness、询问 Human、等待或结束;复杂业务可以拥有领域 CRD |

Expand All @@ -53,6 +53,9 @@ user | harness | operator
外部系统可能把某个执行目标称为 Agent、Assistant 或 Session;接入 loopd 后统一表现为 Harness。
loopd Core、公共 API、数据库和 UI 不再建立一套与 Harness 平行的 Agent 概念。

公共类型用 `ActorRef {kind, key}` 表达参与者身份,用 `target` 表达本次请求选中的执行目标。`Member` 留给
未来真实存在的 Operator 团队或可见性成员关系,避免把“能参与一次对话”和“隶属于某个集合”混为一谈。

## 3. 核心协作对象

三类参与者通过少量具有稳定身份和生命周期的对象协作:
Expand All @@ -61,7 +64,7 @@ loopd Core、公共 API、数据库和 UI 不再建立一套与 Harness 平行
|---|---|
| **Conversation** | loop-server 拥有的持久协作空间;历史属于 Conversation,不属于某个 Operator 或 Harness |
| **Message** | Conversation 中一次页面可见表达;`task_id + kind + key` 标识问答任务与发送者,content 保存 AgentUE semantic model 快照 |
| **Task** | loop-server 为一次问答创建的通用 CRD;只保存 responder 路由与唤醒版本,名称即 Message 使用的 `task_id` |
| **Task** | loop-server 为一次问答创建的通用 CRD;只保存目标 Actor 路由与唤醒版本,名称即 Message 使用的 `task_id` |
| **Operator Resource** | 复杂 Operator 按需创建并拥有的领域 CRD;记录该领域独有的目标、状态和完成条件 |
| **Harness Execution** | Harness 自己拥有的执行;可耗时、流式返回,完整轨迹由 AgentLedger 记录 |

Expand Down Expand Up @@ -99,7 +102,7 @@ metadata:
spec:
target:
kind: operator | harness
key: <responder-key>
key: <actor-key>
revision: 1
```

Expand All @@ -118,6 +121,7 @@ Auditor,其他 Operator 可以采用完全不同的 Resource;这些领域概
```text
Human
→ create user Message + empty harness Message (same task_id)
→ initialize the same-ID AgentUE event stream
→ create same-ID Task CRD before the database commit
→ Harness starts or resumes its execution
→ project visible stream into the harness Message
Expand All @@ -133,6 +137,7 @@ Harness 的流式文本可以直接构成主回答,可见工具状态可以投
```text
Human
→ create user Message + empty operator Message (same task_id)
→ initialize the same-ID AgentUE event stream
→ create same-ID Task CRD before the database commit
→ Operator Reconciler resolves Conversation context through loop-runtime
→ optionally create or update an Operator-owned domain CRD
Expand All @@ -149,12 +154,16 @@ Operator 内部 Harness 的输出默认属于详情会话或 AgentLedger,不
内容才成为 `operator` 角色的最终回答;Operator 也可以选择把某个 Harness 结果显式公开为 Conversation
Message。

首次观察与断线续接使用同一个 Chat API:请求不带 `task_id` 时创建问答和 Task,带 `task_id` 时观察已有
Task;`Last-Event-ID` 只表示客户端已消费的 Redis transport cursor。HTTP 资源模型不额外暴露 Replay
接口,续接后的 AgentUE delivery 仍然从重建的完整 `start` 开始。

## 6. Conversation Context

Conversation History 是 loop-server 的事实,不属于任何 Operator 或 Harness。同一 Conversation 可以先后
由不同 Operator 和 Harness 参与,它们都可以在授权范围内读取已有历史。

v1alpha1 Task CRD 不复制完整对话,当前保存 responder 路由与唤醒版本。loop-server 根据 Task ID 从主
v1alpha1 Task CRD 不复制完整对话,当前保存目标 Actor 路由与唤醒版本。loop-server 根据 Task ID 从主
Conversation Message 即时组装上下文:

```text
Expand All @@ -177,10 +186,11 @@ Human interaction 的控制骨架:
```text
Chat.Conversation 读取 Conversation
Chat.History 显式读取 Conversation 的增量历史
Chat.Send 创建 user、responder Message 与 Task CRD,并返回 responder Message
Chat.Update 更新 responder Message 的可见语义快照
Chat.Send 首次请求创建两条 Message 与 Task CRD;带 task_id 时续接同一 AgentUE 事件流
Chat.Emit Operator 发布 set/append 页面事件
Chat.Complete 折叠事件并固化 response Message,然后结束事件流
Task.Get 按 Task ID 读取当前 input、response 与 Conversation History
Task.Watch 为指定 responder 注册 Task Reconciler
Task.Watch 为指定 Actor 注册 Task Reconciler
Harness.Prompt 以 prompt、tools 和可选 Harness target 发起或恢复一次 Call
Ask / Confirm 请求 Human 提供信息或确认决定
```
Expand Down Expand Up @@ -233,9 +243,9 @@ loop-server 是页面协作事实和 Task 分发的 owner:

- Conversation 代表一个对话框;
- Message 是页面可见对话历史的事实来源;
- 同一次问答的 user、responder 及后续可见交互共享 `task_id`;
- 同一次问答的 user、response 及后续可见交互共享 `task_id`;
- Message content 可以投影 Activity、Artifact 等页面需要展示的内容。
- Task CRD 以同一个 `task_id` 命名;v1alpha1 当前承载 responder 路由与可推进版本。
- Task CRD 以同一个 `task_id` 命名;v1alpha1 当前承载目标 Actor 路由与可推进版本。

loop-server 不建立 `tasks` 表,也不保存 Operator 领域表。Task 查询是基于 Message 的实时视图;通用
Activity 只承载跨 Operator 都能理解的处理摘要,更丰富的领域详情留在 Operator Resource,必要时通过
Expand All @@ -244,26 +254,27 @@ Activity 只承载跨 Operator 都能理解的处理摘要,更丰富的领域
AgentLedger 记录 prompt、模型事件、Harness Call、tool call/result、重试和成本等完整执行事实,用于审计、
回放和分析;它不承担页面 Conversation History,因此不能替代 loop-server 的两表业务模型。

AgentUE 在 loopd 中定义页面语义模型。AgentUE Runner 当前负责 Python 后台任务、Redis 事件桥和 heartbeat
recovery;直接把它嵌入 Go loop-server 会与 Harness provider、Task 和领域 CRD 形成多个执行 owner。因此
v1 只复用 AgentUE 的页面模型,不把 AgentUE Runner 作为 loopd 的执行 owner。
AgentUE 在 loopd 中定义页面语义模型,并提供不拥有业务任务的 Redis Event Bridge。Operator 通过
loop-runtime 发布 `set/append`;任意 loop-server 实例都可按 `task_id` 和 transport cursor 重建完整
`start` 快照并继续输出。loop-server 在完成阶段把最终快照写入 Message。AgentUE 不创建 Task、不调用
Harness,也不成为 Operator 执行的 owner。

## 10. 关键不变量

1. **公开角色只有三种**:Conversation 中只使用 `user`、`harness`、`operator`;外部 provider 的术语不
扩散到 loopd Core。
2. **Conversation 独立于 responder**:历史属于 Human 持有的 Conversation,不因切换 Operator 或 Harness
2. **Conversation 独立于 Actor**:历史属于 Human 持有的 Conversation,不因切换 Operator 或 Harness
被复制或切断。
3. **Task 从分发和唤醒起步**:v1alpha1 保持最小;后续只增加通用协调字段,不复制 Query、History、
回答和 Operator 领域状态。
4. **领域 CRD 按复杂度引入**:简单 Operator 直接 Reconcile Task;复杂 Operator 自己拥有领域 Resource。
5. **一次问答有稳定 task identity**:初始 user/responder Message 和后续反问、确认共享同一 `task_id`。
5. **一次问答有稳定 task identity**:初始 user/response Message 和后续反问、确认共享同一 `task_id`。
6. **外部动作先获得持久 identity**:同一 owner 与 effect key 的重试必须观察或恢复同一次 Harness Call,
不能重复触发无法证明结果的副作用。
7. **流式响应与完成正交**:有事件表示 Call 有进展,不表示成功;长时间运行也不能被等同于失联。
8. **上下文有明确边界**:TaskContext 给出当前输入、回答和 History 水位;复杂 Operator 可以把所需引用
保存到自己的 Resource。
9. **最终回答由 responder 收口**:直连 Harness 由 Harness 回答;Operator 内部可以调用多个 Harness,但由
9. **最终回答由目标 Actor 收口**:直连 Harness 由 Harness 回答;Operator 内部可以调用多个 Harness,但由
Operator 汇总并回答。
10. **可见历史与完整轨迹分层**:Message 只保存页面可见快照,AgentLedger 保存完整执行轨迹。
11. **Harness 差异止于 Adapter**:新增 agentd 或第三方 Harness 不应要求修改 Conversation 或 Operator 模型。
Expand Down
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ module github.com/compforge/loopd
go 1.26.0

require (
github.com/alicebob/miniredis/v2 v2.39.0
github.com/cloudwego/hertz v0.10.4
github.com/compforge/agentue/sdks/go v0.0.0-20260904102512-0ec4d015e66a
github.com/qiankunli/go-stdx v0.0.4-0.20260824051808-f7f6d7c53de2
github.com/redis/go-redis/v9 v9.22.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
k8s.io/apimachinery v0.36.0
Expand Down Expand Up @@ -35,7 +38,7 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
Expand All @@ -52,6 +55,8 @@ require (
github.com/tidwall/pretty v1.2.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
Expand Down
Loading