yao/agent/robot/V2-IMPROVEMENTS.md
Max bc4787f857 Update executor to support V2 execution model and enhance event handling
- Implement V2 execution model in the standard executor, simplifying task execution to a single call without validation loops.
- Introduce support for resuming suspended executions, allowing for human input during task processing.
- Enhance event handling by pushing task completion and failure events to the event bus for better tracking and integration.
- Update tests to reflect changes in execution flow and ensure robust handling of task statuses and results.
2026-02-25 18:40:48 +08:00

16 KiB

Robot Agent V2 — Improvement Plan

Generated: 2026-02-25 Based on: DESIGN-V2.md deep review against implementation code Scope: Bug fixes, missing unit tests, code quality improvements


Auth Context Clarification

Robot is a legitimate team member in __yao.member. Auth is always present:

Trigger Path Auth Source Code
Clock manager.buildRobotAuth(robot){UserID: robot.MemberID, TeamID: robot.TeamID} manager.go:270
Human / Event Caller's ctx.Auth passthrough from HTTP middleware openapi/agent/robot/*.go
Resume Loaded from execution record → buildRobotAuth or caller passthrough executor.go:764+

The openapi/agent/robot/ layer constructs ctx := &robottypes.Context{} without Auth — this is the existing V1 pattern across ALL openapi handlers (trigger.go, execution.go, list.go, etc). Auth checking is done via authorized.GetInfo(c) at the Gin middleware level; robottypes.Context is a downstream execution context.

However, manager/interact.go:createConfirmingExecution calls ctx.UserID() which returns "" when openapi passes an empty Context. This is a V2-specific issue since V1 handlers don't need ctx.UserID().


1. Bugs

BUG-1 [P0] advanceExecution discards confirmed Goals/Tasks

File: manager/interact.go:415-431

Problem: After multi-round Host Agent confirmation (which may have generated Goals and Tasks stored in record.Goals / record.Tasks), advanceExecution() submits to Pool via m.pool.SubmitWithID(...). The Pool Worker then calls ExecuteWithControl() which starts from P1 (Goals) and re-generates everything — the confirmed plan is lost.

Design intent (§10.1): Confirmation → use confirmed Goals/Tasks → skip P1/P2 → directly execute P3.

Fix: advanceExecution must inject record.Goals and record.Tasks into the TriggerInput or use a dedicated Resume-like path that skips P1/P2 when Goals/Tasks already exist.

Test required:

  • Confirm with pre-existing Goals/Tasks → verify P3 uses those Goals/Tasks, not re-generated ones
  • Confirm without Goals/Tasks → verify normal P1→P2→P3 flow

BUG-2 [P0] standard.New() creates orphan Executor instances

Files: manager/interact.go:501, 525, 544

Problem: skipWaitingTask(), resumeWithContext(), and directResume() all call standard.New(), creating a fresh Executor with independent counters. Consequences:

  1. currentCount / execCount not shared — monitoring inaccurate
  2. No execController.Untrack() after Resume completes — memory leak
  3. Separate store / robotStore instances (less critical, stateless)

Fix: Manager should hold a reference to the live Executor (obtained via Pool) and expose a Resume method, or provide the Executor as a constructor parameter.

Tests required:

  • Resume via skipWaitingTask → verify execController.Untrack() called
  • Resume via resumeWithContext → verify executor currentCount incremented/decremented correctly

BUG-3 [P1] buildRobotStatusSnapshot returns near-empty snapshot

File: manager/interact.go:266-278

Problem: Only populates ActiveCount and MaxQuota. Missing: WaitingCount, QueuedCount, ActiveExecs, RecentExecs. Host Agent cannot make informed decisions about robot workload.

Fix: Query robot.Executions to compute WaitingCount, collect ActiveExecs briefs, and optionally query recent completed executions from store.

Tests required:

  • Robot with 2 running + 1 waiting execution → snapshot reflects correct counts
  • Robot with no executions → all counts zero

BUG-4 [P1] openapi/agent/robot/interact.go passes empty Context to Manager

File: openapi/agent/robot/interact.go:67, 152, 209

Problem: ctx := &robottypes.Context{} — no Auth, no context.Context. When HandleInteractcreateConfirmingExecution calls ctx.UserID(), returns "". The TriggerInput.UserID in the DB record is empty.

Note: This is NOT about Robot's own Auth (which is always set via buildRobotAuth in execution paths). This is about tracking which human user initiated the interaction.

Fix: In V2 interact handlers, construct Context properly:

ctx := robottypes.NewContext(c.Request.Context(), &oauthtypes.AuthorizedInfo{
    UserID: authInfo.UserID,
    TeamID: authInfo.TeamID,
})

Tests required:

  • InteractRobot handler → verify ctx.UserID() returns the authenticated user's ID
  • CreateConfirmingExecution → verify record.Input.UserID is populated

BUG-5 [P2] HostContext.Goals type mismatch with design

File: types/host.go:15

Problem: Design §5.7 defines Goals string, implementation uses *Goals (struct with Content field). Host Agent receives {"goals": {"content": "..."}} instead of {"goals": "..."}.

Fix: Either update the Host Agent prompt to expect the struct format, or flatten to string in buildHostContext:

if record.Goals != nil {
    hostCtx.GoalsContent = record.Goals.Content  // string
}

Tests required:

  • buildHostContext with Goals → verify JSON output matches Host Agent prompt expectations

2. Missing Unit Tests

All tests should be black-box tests (test exported APIs only), must verify return values and side effects, and must not require real LLM calls.

2.1 executor/standard/host.go — CallHostAgent

Current coverage: 0 tests

# Test Case Verify
H1 Robot is nil Returns error "robot cannot be nil"
H2 No Host Agent configured (empty Resources) Returns error "no Host Agent configured"
H3 Valid Host Agent call returns JSON Parsed HostOutput with correct Action and Reply
H4 Host Agent returns non-JSON text Fallback to HostActionConfirm with text as Reply
H5 Host Agent returns invalid JSON structure Fallback to HostActionConfirm
H6 Host Agent call fails (network error) Returns wrapped error
H7 Input marshalling (verify HostInput fields) Correct JSON sent to agent

Status: All tests implemented. H1-H2, H7 are pure unit tests. H3-H5 use real LLM integration via yao-dev-app test assistants (tests.host-json, tests.host-plaintext, tests.host-badjson). H6 uses real assistant framework.


2.2 manager/interact.go — processHostAction (all branches)

Current coverage: 2/7 branches (WaitForMore, default)

# Test Case Action Verify
PA1 HostActionConfirm confirm resp.Status == "confirmed", advanceExecution called
PA2 HostActionAdjust with goals data adjust Record Goals updated, resp.Status == "adjusted"
PA3 HostActionAdjust with tasks data adjust Record Tasks updated
PA4 HostActionAdjust with nil data adjust No error, noop
PA5 HostActionAddTask add_task New task appended to record.Tasks with generated ID
PA6 HostActionAddTask with nil data add_task Returns error "task data is required"
PA7 HostActionSkip with waiting task skip Task status = skipped
PA8 HostActionSkip without waiting task skip Returns error "no task is waiting"
PA9 HostActionInjectCtx with string reply inject_context Resume called with correct reply
PA10 HostActionInjectCtx → re-suspend inject_context resp.Status == "waiting"
PA11 HostActionCancel cancel Execution status = cancelled, event pushed
PA12 WaitForMore = true resp.Status == "waiting_for_more", resp.WaitForMore == true
PA13 Unknown action resp.Status == "acknowledged"

Note: PA1, PA7, PA9, PA11 require mocking Executor.Resume and Pool.SubmitWithID.


2.3 manager/interact.go — HandleInteract routing

Current coverage: Parameter validation only

# Test Case Verify
HI1 No execution_id → creates confirming execution Record saved with status=confirming, Host Agent called with "assign"
HI2 execution_id with status=confirming Host Agent called with "assign" scenario
HI3 execution_id with status=waiting Host Agent called with "clarify" scenario
HI4 execution_id with status=running Host Agent called with "guide" scenario
HI5 execution_id with status=completed Returns error "cannot interact"
HI6 execution_id not found Returns error "execution not found"
HI7 Host Agent unavailable → direct assign fallback Execution started without Host Agent
HI8 Host Agent unavailable → direct resume fallback Execution resumed directly

2.4 manager/interact.go — CancelExecution

Current coverage: "manager not started" only

# Test Case Verify
CE1 Cancel waiting execution Status → cancelled, Untrack called, event pushed
CE2 Cancel confirming execution Status → cancelled
CE3 Cancel running execution Returns error (only waiting/confirming allowed)
CE4 Cancel non-existent execution Returns error "execution not found"
CE5 Cancel already cancelled Returns error

2.5 executor/standard/executor.go — Resume method

Current coverage: Only via E2E tests (requires real LLM)

# Test Case Verify
R1 Resume non-waiting execution Returns error "not in waiting status"
R2 Resume non-existent execution Returns error "execution not found"
R3 Resume with nil store Returns error "store is required"
R4 Resume injects reply into task messages exec.Tasks[i].Messages contains [Human reply] prefixed message
R5 Resume clears waiting fields WaitingTaskID, WaitingQuestion, WaitingSince all empty after resume
R6 Resume updates status to running exec.Status == ExecRunning
R7 Resume → re-suspend Returns ErrExecutionSuspended, execution stays tracked
R8 Resume → complete → P4 → P5 Status == ExecCompleted, ResumeContext cleared
R9 Resume → P3 error Status == ExecFailed with error message
R10 Resume maintains executor currentCount currentCount +1 before, -1 after

Note: R4-R10 require mocking store.Get, store.UpdateResumeState, RunExecution, runPhase.


2.6 manager/interact.go — Helper methods

Current coverage: buildRobotStatusSnapshot (3), findWaitingTask (3), buildHostContext (2)

# Missing Test Case Verify
HL1 createConfirmingExecution Record has correct fields (execID, chatID, status=confirming, input)
HL2 adjustExecution with goals string record.Goals.Content updated
HL3 adjustExecution with tasks array record.Tasks replaced
HL4 adjustExecution with non-map data Graceful handling
HL5 injectTask with valid task Task appended with auto-generated ID
HL6 injectTask preserves existing tasks len(tasks) == original + 1
HL7 callHostAgentForScenario — no host agent Returns error
HL8 directAssign Returns "confirmed" status
HL9 directResume — re-suspend Returns "waiting" status
HL10 directResume — complete Returns "resumed" status

2.7 api/interact.go — Interact/Reply/Confirm/CancelExecution

Current coverage: 0 tests

# Test Case Verify
AI1 Interact with manager available Delegates to managerInteract
AI2 Interact without manager, with execution_id Delegates to legacyResume
AI3 Interact without manager, without execution_id Returns error
AI4 Interact with empty member_id Returns error
AI5 Interact with nil request Returns error
AI6 Reply shortcut Calls Interact with correct TaskID and Source
AI7 Confirm shortcut Calls Interact with correct Action
AI8 CancelExecution with manager Delegates correctly
AI9 CancelExecution without manager Returns error
AI10 legacyResume → success Returns "resumed" status
AI11 legacyResume → re-suspend Returns "waiting" status
AI12 legacyResume → error Returns wrapped error

2.8 events/events.go + events/handlers.go — Event integration

Current coverage: DeliveryHandler basic (3 tests)

# Missing Test Case Verify
EV1 DeliveryHandler — payload deserialization All fields (ExecutionID, MemberID, Content, Preferences) correctly parsed
EV2 Verify event constants match design §7.2 All 9 constants present and correctly named
EV3 NeedInputPayload marshalling Correct JSON roundtrip
EV4 TaskPayload marshalling Correct JSON roundtrip with optional Error field
EV5 ExecPayload marshalling Correct JSON roundtrip

2.9 openapi/agent/robot/interact.go — HTTP handlers

Current coverage: 0 tests

# Test Case Verify
OH1 InteractRobot — valid request 200 with InteractResponse
OH2 InteractRobot — missing robot ID 400 error
OH3 InteractRobot — missing message 400 error
OH4 InteractRobot — robot not found 404 error
OH5 InteractRobot — forbidden (no write permission) 403 error
OH6 ReplyToTask — valid request 200 with response
OH7 ReplyToTask — missing params 400 error
OH8 ConfirmExecution — valid request 200 with response
OH9 ConfirmExecution — empty body allowed 200 (confirm without message)

2.10 Event push verification in execution flow

Current coverage: 0 (events are pushed but never verified in tests)

# Test Case File Verify
EP1 Task completes → TaskCompleted event run.go:111 Event type + payload fields
EP2 Task fails → TaskFailed event run.go:120 Event type + error in payload
EP3 Execution suspends → ExecWaiting event executor.go:750 Event type + question in payload
EP4 Execution resumes → ExecResumed event executor.go:856 Event type + chatID
EP5 Execution completes → ExecCompleted event executor.go:287 Event type + status
EP6 Execution cancelled → ExecCancelled event manager/interact.go:66 Event type + status
EP7 Delivery → Delivery event delivery.go:102 Content + Preferences in payload

Approach: Use event.Subscribe in test to capture pushed events, or mock event.Push.


3. Code Quality Improvements

CQ1 — Extract common Executor resume logic

skipWaitingTask, resumeWithContext, directResume all duplicate: create executor → call Resume → handle ErrExecutionSuspended. Extract to a private helper:

func (m *Manager) executeResume(ctx *types.Context, execID, reply string) error {
    // Use shared executor reference, not standard.New()
    return m.getExecutor().Resume(ctx, execID, reply)
}

CQ2 — processHostAction needs explicit store.Save() after Confirm

advanceExecution changes execution status but doesn't save the Goals/Tasks that may have been set during confirming flow. Needs explicit persist before Pool submit.

CQ3 — RobotStatusSnapshot should include MemberID and Status

Add back MemberID and Status fields to match design §5.7. These help Host Agent identify which robot it's serving.


4. Implementation Priority

Priority Items Est. Effort
P0 BUG-1 (advanceExecution), BUG-2 (standard.New) 1 day
P1 BUG-3 (snapshot), BUG-4 (context auth) 0.5 day
P1 Tests §2.2 (processHostAction), §2.3 (HandleInteract), §2.5 (Resume) 1.5 days
P2 BUG-5 (Goals type), CQ1-CQ3 0.5 day
P2 Tests §2.1 (CallHostAgent), §2.4 (Cancel), §2.6-2.10 2 days
Total ~5.5 days

5. Test Infrastructure Notes

  1. Source env before test: source yao/env.local.sh
  2. Test app: yao-dev-app — all test assistants live there
  3. No recompile needed: yao-dev runs from Go source directly
  4. Mock strategy: For unit tests not requiring real LLM, create interfaces for ConversationCaller, ExecutionStore, Pool to enable mock injection. Alternatively, use SkipPersistence: true config + in-memory stubs.
  5. Event verification: Wrap event.Push calls with a test interceptor or use event.Subscribe to capture events during test.