Skip to content

feat(agent-engine): add phase 2 orchestration and runtime provisioning - #493

Merged
xxx7xxxx merged 2 commits into
OpenCSGs:refactoringfrom
xxx7xxxx:refactoring-agent-engine
Aug 19, 2026
Merged

feat(agent-engine): add phase 2 orchestration and runtime provisioning#493
xxx7xxxx merged 2 commits into
OpenCSGs:refactoringfrom
xxx7xxxx:refactoring-agent-engine

Conversation

@xxx7xxxx

Copy link
Copy Markdown
Collaborator

Summary

  • implement the complete Phase 2 Agent Engine contract, lifecycle coordination, and Session API integration
  • add Codex file, interaction, cancellation, structured-output, credentials, and initShell runtime support
  • add shared real-Engine and MemoryClient contracts plus a mock-backed Feishu adapter harness

conversationKey: agentengine.ConversationKey(binding.ConversationKey),
turnID: agentengine.TurnID(responseID),
}
h.addSessionTurn(turnKey, turn)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The turn is exposed to the cancel endpoint before Engine.Run has registered it. A cancellation in this window makes Cancel return success without finding the turn, after which the request proceeds normally. Consider making registration and publication atomic, or retaining a request-level cancel function until Engine admission completes, and add a regression test for this race.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a569f45. The Session turn index now stores the request cancel function before publishing the turn. The cancel endpoint cancels that context before exact Engine cancellation, so a cancel in the pre-registration window prevents Run from being admitted or dispatched. A deterministic regression test verifies the endpoint returns 204 and the Engine records zero calls.

return result
}
case event, ok := <-events:
if !ok {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once events is closed, this receive case remains permanently ready and the loop spins until PromptTurn returns or the context ends. If the subscription closes while the prompt is still blocked, one turn can consume a CPU core for the entire timeout. Set events = nil after observing closure (or return an explicit runtime failure) so the select blocks on the remaining signals.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a569f45. When the Runtime event subscription closes, the adapter now sets the channel to nil, so select waits on PromptTurn or context instead of spinning. A regression test uses a closed event stream and blocked prompt to verify Run remains pending and completes normally after the prompt returns.

@xxx7xxxx
xxx7xxxx force-pushed the refactoring-agent-engine branch from 6976bf8 to a569f45 Compare August 18, 2026 09:54
Comment thread internal/agentengine/agent_facade.go Outdated
}
currentRuntime := previous.RuntimeConfig()
desiredRuntime := createAgentSpec(spec).RuntimeConfig()
replacesRuntime := currentRuntime != desiredRuntime || previous.Role != string(spec.Role)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Role changes are routed through Service.Create(... Replace: true), but that service treats any replacement involving a manager as an operation on the singleton manager. Updating a worker to manager can therefore return/update the manager while leaving the requested worker unchanged; updating the manager to worker also remains on the manager path. Since Update promises to replace the complete AgentSpec, please either implement role transitions for the addressed resource or reject role changes explicitly, and cover both directions in the shared contract tests.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7363282. Agent Engine Update now rejects role changes before any Skill or Runtime mutation, and MemoryClient exposes the same behavior. The shared contract covers both worker-to-manager and manager-to-worker attempts.

if c.agentID == "" || request.ID == "" || request.ConversationKey == "" || len(request.Input) == 0 {
return failed(agentengine.ErrorInvalidRequest, "agent ID, turn ID, conversation key, and input are required")
}
for _, part := range request.Input {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MemoryClient accepts requests that the real Engine rejects: file inputs are not checked for ID/path/name/media type/size/SHA-256, and unsupported continuation or interaction policies are not rejected. An adapter can therefore pass the mock-backed contract suite but fail after switching to the real Engine. Please share the normalization/validation logic or add equivalent checks and contract cases so both implementations expose the same request boundary.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7363282. MemoryClient now normalizes request identity and policies, rejects unsupported continuation and interaction values, and validates the complete Runtime-neutral file shape. The same shared contract cases now run against MemoryClient and the real Engine.

return Agent{}, err
}
}
if err := f.service.ReplaceSkills(ctx, created.ID, spec.Skills); err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A manager Create is backed by EnsureManager, so created may be the pre-existing singleton manager rather than a resource created by this call. If skill validation/staging then fails (for example, an unknown skill name), this cleanup deletes that existing manager, its runtime, and its home directory. Please validate/stage skills before creating anything, or track whether this call actually created the resource and never delete a pre-existing manager during rollback; add a regression test starting with an existing manager.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7363282. Manager Create records whether the singleton existed before the call and only performs destructive rollback for a newly created resource. A regression test starts with an existing manager, forces Skill staging failure, and verifies the manager and Runtime are preserved.

Comment thread internal/agentengine/runtime_adapter.go Outdated
if err != nil {
return nil, cleanup, &TurnError{Code: ErrorFileUnavailable, Message: err.Error()}
}
root := filepath.Join(workspace, ".csgclaw", "engine-inputs")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The destination is inside the Runtime-controlled workspace, but these host-side path operations follow symlinked parent components. A Runtime can replace .csgclaw or engine-inputs with a symlink and make the host copy an authorized input outside this workspace; racing the deferred RemoveAll can also redirect cleanup outside the intended root. Please anchor creation, writes, and cleanup with os.Root (as the credential provisioning code does) or otherwise reject symlink components, and cover a malicious destination-symlink case.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7363282. Runtime-local input staging is now anchored to the selected workspace with os.Root; directory creation, file writes, rename, and cleanup all use root-relative operations. A malicious .csgclaw destination symlink test verifies no file is created outside the workspace.

return result
}
case event, ok := <-events:
if !ok {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disabling the closed channel fixes the spin, but the turn is still reported as successful once PromptTurn returns even though all remaining output events were lost. Because TurnResult.Output is built only from this stream, a subscriber failure can become a successful empty response. Treat an unexpected event-stream closure before PromptCompleted as a Runtime failure (while still waiting for Runtime cleanup), and update the new closure test to assert that terminal result.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7363282. Premature event-stream closure now disables the channel, waits for PromptTurn cleanup, and returns a dispatched runtime_failed result instead of successful empty output. The closure regression test asserts the terminal failure.

Comment thread internal/agentengine/runtime_adapter.go Outdated
<-promptDone
promptReturned = true
}
result.Dispatched = dispatched

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dispatched is updated only when the select consumes accepted (or promptDone), so an accepted turn can take an event-failure or context-cancellation branch first and reach stopPrompt with this stale value still false. That violates the documented rule that every outcome after Runtime submission retains Dispatched=true, and can make a caller retry a turn that already ran. Reload dispatchedState in stopPrompt after waiting for prompt cleanup, and add a test where OnAccepted fires before a failing event/cancellation but the accepted case is not consumed first.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0d05fc4. Terminal cleanup now reloads the atomic acceptance state after waiting for PromptTurn cleanup instead of using the potentially stale local dispatched value. A 100-iteration accepted-then-failed-event regression verifies every post-submission failure retains Dispatched=true.

@xxx7xxxx
xxx7xxxx force-pushed the refactoring-agent-engine branch 3 times, most recently from 0d05fc4 to 0fb19c2 Compare August 19, 2026 01:19
@GatewayJ

Copy link
Copy Markdown
Member
  1. TurnID 幂等
    当前 Engine 只记录活跃 Turn,结束即删除。因此飞书网络重试同一事件时,会导致第二次执行 Agent。

  2. Reset 必须成为原子控制操作
    当前 Reset 遇到活跃 Turn 直接返回 conversation_busy。渠道虽可先 Cancel 再 Reset,但会留下竞态窗口:Cancel 完成后,另一入口可能先提交新 Turn。
    建议让 Reset 在同一 Conversation gate 内:取消当前 Turn → 等待清理 → Reset Runtime mapping → 放开新 admission。

  3. Resolve 必须原子 claim
    Resolve 先读取 interaction,解锁后调用 runtime,成功后才删除。重复飞书卡片 action 可以同时通过检查。
    建议在锁内将 interaction 从 pending 移到 resolving/consumed,只有第一个请求可进入 runtime;失败时按明确策略恢复或终止。飞书用户身份授权由 Channel Adapter 完成,Engine 只保存不透明 ResponderID 用于审计即可。

  4. 完整事件信封
    TurnEvent 目前只有本次调用内的 Sequence。建议补充:
    TurnID
    Sequence
    ResumeCursor // 可选
    Terminal // final output / recoverable failure

@GatewayJ

Copy link
Copy Markdown
Member

admission 目前固定 reject;如果飞书需要排队、supersede,建议在 TurnRequest 加显式 AdmissionPolicy,而不是让每个渠道各自实现执行队列。

@jialudev

Copy link
Copy Markdown
Collaborator

Looks good to merge as the Phase 2 baseline. We can refine the remaining details as we integrate the Channel and Runtime adapters.

@xxx7xxxx
xxx7xxxx force-pushed the refactoring-agent-engine branch from 0fb19c2 to 623348a Compare August 19, 2026 04:34
@xxx7xxxx

Copy link
Copy Markdown
Collaborator Author

@GatewayJ

感谢反馈,当前处理如下:

  1. 已实现 TurnID 进程内幂等:并发重试复用 Active Turn,已分派的完成结果进入有界缓存并可重放;未实现跨重启幂等,因为这需要引入持久化 Turn Store,超出 Engine 当前职责。
  2. 已实现原子 Reset:在同一 Conversation Gate 内依次关闭 Admission、取消并等待 Active Turn 清理、Reset Runtime Mapping,最后重新开放 Admission。
  3. 已实现 Resolve 原子 Claim:Interaction 在锁内从 pending 进入 resolving,只有第一个请求会调用 Runtime,失败时恢复 pending;ResponderID 仅作为不透明审计身份传递。
  4. 部分实现完整事件信封:TurnEvent 已增加 TurnID,并保留现有 Sequence。未实现 ResumeCursor,因为当前没有 Resume 请求入口或持久化 Event Log;未在 Event 中增加 Terminal TurnResult,因为 Run 已经同步返回唯一 TurnResult,增加它会形成重复的终态协议。

第二个单独的评论:

已在 TurnRequest 增加 AdmissionPolicy,并实现 reject_if_busywaitsupersede。Session API 使用 reject,Feishu Harness 使用 supersede;wait 当前是等待后重新竞争 Admission,未实现严格的 FIFO Queue,因为目前没有明确的排序、容量和 Queue Timeout 要求。后续如果有更明确的需求,再加强FIFO的 wait 功能。

@xxx7xxxx
xxx7xxxx changed the base branch from main to refactoring August 19, 2026 07:00
@xxx7xxxx
xxx7xxxx merged commit c7e915a into OpenCSGs:refactoring Aug 19, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants