- 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.
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:
currentCount/execCountnot shared — monitoring inaccurate- No
execController.Untrack()after Resume completes — memory leak - Separate
store/robotStoreinstances (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→ verifyexecController.Untrack()called - Resume via
resumeWithContext→ verify executorcurrentCountincremented/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 HandleInteract → createConfirmingExecution 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:
buildHostContextwith 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
- Source env before test:
source yao/env.local.sh - Test app:
yao-dev-app— all test assistants live there - No recompile needed:
yao-devruns from Go source directly - Mock strategy: For unit tests not requiring real LLM, create interfaces for
ConversationCaller,ExecutionStore,Poolto enable mock injection. Alternatively, useSkipPersistence: trueconfig + in-memory stubs. - Event verification: Wrap
event.Pushcalls with a test interceptor or useevent.Subscribeto capture events during test.