nandi/oripublic Fork 0
2434cc7fb724f02b34c0189dfd5fb30c1a8a68e3
Commits
Clone
git clone https://git.rickub.com/nandi/ori.git
git clone ssh://git@rickub.com/nandi/ori.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

forked from bots-garden/ori

mockagent.go · 235 lines · 7.5 KBGo Blame HistoryRaw
✨ Introduce new feature(s): ACP web client — Go backend (agent, bridge, httpserver, mockagent) + React SPA (Zed-like agent panel), tests, quality gate PASS 2434cc7 k33g yesterday1// Package mockagent implements a small, deterministic ACP agent used to
2// exercise and demo ori without any real AI agent behind it: it streams a
3// thought, a plan, tool calls (including a file diff), asks one permission,
4// then answers. The ori-mock-agent command exposes it on stdio.
5package mockagent
6
7import (
8 "context"
9 "fmt"
10 "io"
11 "strconv"
12 "sync"
13 "time"
14
15 acp "github.com/coder/acp-go-sdk"
16)
17
18// Agent is the mock ACP agent. The embedded acp.Agent interface covers the
19// protocol methods ori's client never calls (session/load, logout, ...);
20// invoking one of them would panic, which is acceptable for a test double.
21type Agent struct {
22 acp.Agent
23
24 // Delay spaces the streamed updates out so a human watching the UI sees
25 // the streaming happen. Tests keep it at zero.
26 Delay time.Duration
27
28 conn *acp.AgentSideConnection
29 mu sync.Mutex
30 sessions int
31}
32
33// New returns a mock agent; wire it with SetAgentConnection before use.
34func New() *Agent { return &Agent{} }
35
36// SetAgentConnection injects the connection used to push updates and ask
37// permissions. The SDK does not do this automatically.
38func (a *Agent) SetAgentConnection(conn *acp.AgentSideConnection) { a.conn = conn }
39
40// Run serves the mock agent over the given transport (stdio in the command)
41// and blocks until the peer disconnects.
42//
43// Example:
44//
45// agent := mockagent.New()
46// if err := agent.Run(os.Stdin, os.Stdout); err != nil {
47// log.Fatal(err)
48// }
49func (a *Agent) Run(in io.Reader, out io.Writer) error {
50 conn := acp.NewAgentSideConnection(a, out, in)
51 a.SetAgentConnection(conn)
52 <-conn.Done()
53 return nil
54}
55
56func (a *Agent) Initialize(_ context.Context, params acp.InitializeRequest) (acp.InitializeResponse, error) {
57 return acp.InitializeResponse{ProtocolVersion: params.ProtocolVersion}, nil
58}
59
60func (a *Agent) Authenticate(context.Context, acp.AuthenticateRequest) (acp.AuthenticateResponse, error) {
61 return acp.AuthenticateResponse{}, nil
62}
63
64func (a *Agent) NewSession(context.Context, acp.NewSessionRequest) (acp.NewSessionResponse, error) {
65 a.mu.Lock()
66 defer a.mu.Unlock()
67 a.sessions++
68 return acp.NewSessionResponse{SessionId: acp.SessionId("mock-" + strconv.Itoa(a.sessions))}, nil
69}
70
71// Cancel is a no-op: the SDK already cancels the context of the running
72// Prompt when session/cancel arrives, which the scenario checks at each step.
73func (a *Agent) Cancel(context.Context, acp.CancelNotification) error { return nil }
74
75// Prompt plays the demo scenario and streams it to the client.
76func (a *Agent) Prompt(ctx context.Context, params acp.PromptRequest) (acp.PromptResponse, error) {
77 scenario := &scenario{agent: a, ctx: ctx, sessionId: params.SessionId, prompt: promptText(params)}
78 stop, err := scenario.play()
79 if err != nil {
80 if ctx.Err() != nil {
81 return acp.PromptResponse{StopReason: acp.StopReasonCancelled}, nil
82 }
83 return acp.PromptResponse{}, err
84 }
85 return acp.PromptResponse{StopReason: stop}, nil
86}
87
88// promptText joins the text blocks of the user's prompt.
89func promptText(params acp.PromptRequest) string {
90 text := ""
91 for _, block := range params.Prompt {
92 if block.Text != nil {
93 text += block.Text.Text
94 }
95 }
96 return text
97}
98
99// scenario streams one full demo turn step by step.
100type scenario struct {
101 agent *Agent
102 ctx context.Context
103 sessionId acp.SessionId
104 prompt string
105}
106
107func (s *scenario) play() (acp.StopReason, error) {
108 if err := s.runSteps(s.sayThought, s.sendPlan, s.sendReadToolCall, s.sayIntro); err != nil {
109 return "", err
110 }
111
112 allowed, err := s.askPermission()
113 if err != nil {
114 return "", err
115 }
116 if !allowed {
117 return acp.StopReasonRefusal, s.runSteps(s.sayRefused)
118 }
119
120 return acp.StopReasonEndTurn, s.runSteps(s.sendEditToolCall, s.saySummary, s.completePlan)
121}
122
123func (s *scenario) sayThought() error {
124 return s.update(acp.UpdateAgentThoughtText("The user wants a demo. Let me show every panel feature."))
125}
126
127func (s *scenario) sayIntro() error {
128 return s.update(acp.UpdateAgentMessageText("I looked at the project. "))
129}
130
131func (s *scenario) sayRefused() error {
132 return s.update(acp.UpdateAgentMessageText("Understood, I will not write the file."))
133}
134
135func (s *scenario) saySummary() error {
136 return s.update(acp.UpdateAgentMessageText(fmt.Sprintf("Done! You said: **%s**\n\n- streamed thought ✅\n- plan ✅\n- tool calls ✅\n- permission ✅", s.prompt)))
137}
138
139// runSteps executes scenario actions in order, stopping at the first error.
140func (s *scenario) runSteps(steps ...func() error) error {
141 for _, step := range steps {
142 if err := s.step(step); err != nil {
143 return err
144 }
145 }
146 return nil
147}
148
149// step runs one scenario action, respecting cancellation and pacing.
150func (s *scenario) step(action func() error) error {
151 if err := s.ctx.Err(); err != nil {
152 return err
153 }
154 if s.agent.Delay > 0 {
155 select {
156 case <-time.After(s.agent.Delay):
157 case <-s.ctx.Done():
158 return s.ctx.Err()
159 }
160 }
161 return action()
162}
163
164func (s *scenario) update(u acp.SessionUpdate) error {
165 return s.agent.conn.SessionUpdate(s.ctx, acp.SessionNotification{SessionId: s.sessionId, Update: u})
166}
167
168func (s *scenario) sendPlan() error {
169 return s.update(acp.UpdatePlan(
170 acp.PlanEntry{Content: "Inspect the project", Priority: acp.PlanEntryPriorityHigh, Status: acp.PlanEntryStatusInProgress},
171 acp.PlanEntry{Content: "Write hello.txt", Priority: acp.PlanEntryPriorityMedium, Status: acp.PlanEntryStatusPending},
172 ))
173}
174
175func (s *scenario) completePlan() error {
176 return s.update(acp.UpdatePlan(
177 acp.PlanEntry{Content: "Inspect the project", Priority: acp.PlanEntryPriorityHigh, Status: acp.PlanEntryStatusCompleted},
178 acp.PlanEntry{Content: "Write hello.txt", Priority: acp.PlanEntryPriorityMedium, Status: acp.PlanEntryStatusCompleted},
179 ))
180}
181
182func (s *scenario) sendReadToolCall() error {
183 if err := s.update(acp.StartToolCall(
184 "read-1", "Reading README.md",
185 acp.WithStartKind(acp.ToolKindRead),
186 acp.WithStartStatus(acp.ToolCallStatusInProgress),
187 acp.WithStartLocations([]acp.ToolCallLocation{{Path: "README.md"}}),
188 )); err != nil {
189 return err
190 }
191 return s.step(func() error {
192 return s.update(acp.UpdateToolCall(
193 "read-1",
194 acp.WithUpdateStatus(acp.ToolCallStatusCompleted),
195 acp.WithUpdateContent([]acp.ToolCallContent{acp.ToolContent(acp.TextBlock("# Ori\n> demo project"))}),
196 ))
197 })
198}
199
200func (s *scenario) sendEditToolCall() error {
201 if err := s.update(acp.StartToolCall(
202 "edit-1", "Writing hello.txt",
203 acp.WithStartKind(acp.ToolKindEdit),
204 acp.WithStartStatus(acp.ToolCallStatusInProgress),
205 acp.WithStartLocations([]acp.ToolCallLocation{{Path: "hello.txt"}}),
206 )); err != nil {
207 return err
208 }
209 return s.step(func() error {
210 return s.update(acp.UpdateToolCall(
211 "edit-1",
212 acp.WithUpdateStatus(acp.ToolCallStatusCompleted),
213 acp.WithUpdateContent([]acp.ToolCallContent{acp.ToolDiffContent("hello.txt", "hello from the ori mock agent\n")}),
214 ))
215 })
216}
217
218// askPermission asks the client whether the mock may "write" a file and
219// interprets the outcome.
220func (s *scenario) askPermission() (bool, error) {
221 resp, err := s.agent.conn.RequestPermission(s.ctx, acp.RequestPermissionRequest{
222 SessionId: s.sessionId,
223 ToolCall: acp.ToolCallUpdate{ToolCallId: "edit-1", Title: strPtr("Write hello.txt")},
224 Options: []acp.PermissionOption{
225 {OptionId: "allow-once", Name: "Allow once", Kind: acp.PermissionOptionKindAllowOnce},
226 {OptionId: "reject-once", Name: "Reject", Kind: acp.PermissionOptionKindRejectOnce},
227 },
228 })
229 if err != nil {
230 return false, err
231 }
232 return resp.Outcome.Selected != nil && resp.Outcome.Selected.OptionId == "allow-once", nil
233}
234
235func strPtr(s string) *string { return &s }