@pi-in-go/pigpen-acp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/CREDITS.md +23 -0
  2. package/LICENSE +22 -0
  3. package/README.md +110 -0
  4. package/extensions/acp/cmd/pig-acp/e2e_llm_test.go +168 -0
  5. package/extensions/acp/cmd/pig-acp/e2e_test.go +541 -0
  6. package/extensions/acp/cmd/pig-acp/go.mod +6 -0
  7. package/extensions/acp/cmd/pig-acp/internal/acp/agent.go +784 -0
  8. package/extensions/acp/cmd/pig-acp/internal/acp/agent_builtin_test.go +536 -0
  9. package/extensions/acp/cmd/pig-acp/internal/acp/agent_commands_test.go +199 -0
  10. package/extensions/acp/cmd/pig-acp/internal/acp/agent_config_test.go +530 -0
  11. package/extensions/acp/cmd/pig-acp/internal/acp/agent_helpers_test.go +215 -0
  12. package/extensions/acp/cmd/pig-acp/internal/acp/agent_load_test.go +333 -0
  13. package/extensions/acp/cmd/pig-acp/internal/acp/agent_prompt.go +289 -0
  14. package/extensions/acp/cmd/pig-acp/internal/acp/agent_session_test.go +429 -0
  15. package/extensions/acp/cmd/pig-acp/internal/acp/auth.go +61 -0
  16. package/extensions/acp/cmd/pig-acp/internal/acp/bench_test.go +47 -0
  17. package/extensions/acp/cmd/pig-acp/internal/acp/helpers_test.go +421 -0
  18. package/extensions/acp/cmd/pig-acp/internal/acp/main_test.go +28 -0
  19. package/extensions/acp/cmd/pig-acp/internal/acp/mutation_extra_test.go +142 -0
  20. package/extensions/acp/cmd/pig-acp/internal/acp/paths.go +77 -0
  21. package/extensions/acp/cmd/pig-acp/internal/acp/pisessions.go +317 -0
  22. package/extensions/acp/cmd/pig-acp/internal/acp/protocol.go +188 -0
  23. package/extensions/acp/cmd/pig-acp/internal/acp/server.go +218 -0
  24. package/extensions/acp/cmd/pig-acp/internal/acp/server_test.go +386 -0
  25. package/extensions/acp/cmd/pig-acp/internal/acp/session.go +1068 -0
  26. package/extensions/acp/cmd/pig-acp/internal/acp/session_diff_test.go +183 -0
  27. package/extensions/acp/cmd/pig-acp/internal/acp/session_events_test.go +443 -0
  28. package/extensions/acp/cmd/pig-acp/internal/acp/session_extra_test.go +547 -0
  29. package/extensions/acp/cmd/pig-acp/internal/acp/session_usage_test.go +198 -0
  30. package/extensions/acp/cmd/pig-acp/internal/acp/settings.go +79 -0
  31. package/extensions/acp/cmd/pig-acp/internal/acp/slash.go +278 -0
  32. package/extensions/acp/cmd/pig-acp/internal/acp/slash_test.go +128 -0
  33. package/extensions/acp/cmd/pig-acp/internal/acp/startup.go +156 -0
  34. package/extensions/acp/cmd/pig-acp/internal/acp/store.go +118 -0
  35. package/extensions/acp/cmd/pig-acp/internal/acp/store_paths_test.go +466 -0
  36. package/extensions/acp/cmd/pig-acp/internal/acp/translate.go +352 -0
  37. package/extensions/acp/cmd/pig-acp/internal/acp/translate_test.go +105 -0
  38. package/extensions/acp/cmd/pig-acp/internal/acp/twin_test.go +28 -0
  39. package/extensions/acp/cmd/pig-acp/internal/acp/types.go +202 -0
  40. package/extensions/acp/cmd/pig-acp/internal/jsonrpc/bench_test.go +24 -0
  41. package/extensions/acp/cmd/pig-acp/internal/jsonrpc/jsonrpc.go +302 -0
  42. package/extensions/acp/cmd/pig-acp/internal/jsonrpc/jsonrpc_test.go +320 -0
  43. package/extensions/acp/cmd/pig-acp/internal/jsonrpc/twin_test.go +28 -0
  44. package/extensions/acp/cmd/pig-acp/internal/pirpc/kill_unix.go +19 -0
  45. package/extensions/acp/cmd/pig-acp/internal/pirpc/kill_windows.go +11 -0
  46. package/extensions/acp/cmd/pig-acp/internal/pirpc/process.go +530 -0
  47. package/extensions/acp/cmd/pig-acp/internal/pirpc/process_test.go +355 -0
  48. package/extensions/acp/cmd/pig-acp/internal/pirpc/twin_test.go +28 -0
  49. package/extensions/acp/cmd/pig-acp/main.go +177 -0
  50. package/extensions/acp/cmd/pig-acp/main_test.go +213 -0
  51. package/extensions/acp/cmd/pig-acp/sigpipe_unix.go +10 -0
  52. package/extensions/acp/cmd/pig-acp/sigpipe_windows.go +5 -0
  53. package/extensions/acp/cmd/pig-acp/testdata/ask/extension.go +28 -0
  54. package/extensions/acp/cmd/pig-acp/testdata/ask/go.mod +7 -0
  55. package/extensions/acp/cmd/pig-acp/testdata/ask/go.sum +2 -0
  56. package/extensions/acp/cmd/pig-acp/testdata/gate/extension.go +37 -0
  57. package/extensions/acp/cmd/pig-acp/testdata/gate/go.mod +7 -0
  58. package/extensions/acp/cmd/pig-acp/testdata/gate/go.sum +2 -0
  59. package/extensions/acp/cmd/pig-acp/twin_test.go +28 -0
  60. package/extensions/acp/extension.go +63 -0
  61. package/extensions/acp/extension_test.go +111 -0
  62. package/extensions/acp/fakehost_test.go +548 -0
  63. package/extensions/acp/go.mod +6 -0
  64. package/extensions/acp/go.sum +2 -0
  65. package/package.json +41 -0
  66. package/port/PORT.md +147 -0
  67. package/port/interop/package-lock.json +89 -0
  68. package/port/interop/package.json +13 -0
  69. package/port/interop/run.mjs +229 -0
  70. package/port/mutate.mjs +110 -0
  71. package/port/mutation-results.txt +107 -0
  72. package/port/mutations.json +748 -0
  73. package/port/oracle/pi-acp/LICENSE +21 -0
  74. package/port/oracle/pi-acp/README.md +209 -0
  75. package/port/oracle/pi-acp/package.json +65 -0
  76. package/port/oracle/pi-acp/src/acp/agent.ts +1661 -0
  77. package/port/oracle/pi-acp/src/acp/auth-required.ts +37 -0
  78. package/port/oracle/pi-acp/src/acp/auth.ts +59 -0
  79. package/port/oracle/pi-acp/src/acp/paths.ts +15 -0
  80. package/port/oracle/pi-acp/src/acp/pi-commands.ts +59 -0
  81. package/port/oracle/pi-acp/src/acp/pi-sessions.ts +333 -0
  82. package/port/oracle/pi-acp/src/acp/pi-settings.ts +75 -0
  83. package/port/oracle/pi-acp/src/acp/session-store.ts +68 -0
  84. package/port/oracle/pi-acp/src/acp/session.ts +1078 -0
  85. package/port/oracle/pi-acp/src/acp/slash-commands.ts +197 -0
  86. package/port/oracle/pi-acp/src/acp/translate/bash.ts +100 -0
  87. package/port/oracle/pi-acp/src/acp/translate/pi-messages.ts +17 -0
  88. package/port/oracle/pi-acp/src/acp/translate/pi-tools.ts +51 -0
  89. package/port/oracle/pi-acp/src/acp/translate/prompt.ts +71 -0
  90. package/port/oracle/pi-acp/src/index.ts +82 -0
  91. package/port/oracle/pi-acp/src/pi-rpc/command.ts +16 -0
  92. package/port/oracle/pi-acp/src/pi-rpc/process.ts +432 -0
  93. package/port/oracle/pi-acp/test/component/agent-steering-followup-modes.test.ts +143 -0
  94. package/port/oracle/pi-acp/test/component/session-diff.test.ts +214 -0
  95. package/port/oracle/pi-acp/test/component/session-events.test.ts +1102 -0
  96. package/port/oracle/pi-acp/test/component/session-list-and-load.test.ts +129 -0
  97. package/port/oracle/pi-acp/test/component/session-list-custom-session-dir.test.ts +52 -0
  98. package/port/oracle/pi-acp/test/component/session-list-scoped.test.ts +76 -0
  99. package/port/oracle/pi-acp/test/component/session-load-toolresult.test.ts +68 -0
  100. package/port/oracle/pi-acp/test/component/session-queue-cancel.test.ts +43 -0
  101. package/port/oracle/pi-acp/test/component/session-slash-commands.test.ts +37 -0
  102. package/port/oracle/pi-acp/test/component/session-thinking-modes.test.ts +22 -0
  103. package/port/oracle/pi-acp/test/component/session-title-long-session.test.ts +56 -0
  104. package/port/oracle/pi-acp/test/component/session-updatedAt-message-only.test.ts +57 -0
  105. package/port/oracle/pi-acp/test/helpers/fakes.ts +87 -0
  106. package/port/oracle/pi-acp/test/unit/auth-methods-terminal-auth-meta.test.ts +22 -0
  107. package/port/oracle/pi-acp/test/unit/builtin-commands.test.ts +60 -0
  108. package/port/oracle/pi-acp/test/unit/context-usage.test.ts +204 -0
  109. package/port/oracle/pi-acp/test/unit/merge-commands.test.ts +19 -0
  110. package/port/oracle/pi-acp/test/unit/model-thinking-levels.test.ts +268 -0
  111. package/port/oracle/pi-acp/test/unit/new-session-auth-required-when-no-models.test.ts +51 -0
  112. package/port/oracle/pi-acp/test/unit/new-session-pi-not-found.test.ts +26 -0
  113. package/port/oracle/pi-acp/test/unit/new-session-runtime-startup-errors.test.ts +148 -0
  114. package/port/oracle/pi-acp/test/unit/pi-command.test.ts +44 -0
  115. package/port/oracle/pi-acp/test/unit/pi-commands.test.ts +32 -0
  116. package/port/oracle/pi-acp/test/unit/pi-enable-embed-context-flag.test.ts +35 -0
  117. package/port/oracle/pi-acp/test/unit/pi-messages.test.ts +29 -0
  118. package/port/oracle/pi-acp/test/unit/pi-rpc-request-timeout.test.ts +137 -0
  119. package/port/oracle/pi-acp/test/unit/pi-rpc-session-path.test.ts +93 -0
  120. package/port/oracle/pi-acp/test/unit/pi-tools.test.ts +40 -0
  121. package/port/oracle/pi-acp/test/unit/prompt-to-pi-message.test.ts +74 -0
  122. package/port/oracle/pi-acp/test/unit/session-config-options.test.ts +217 -0
  123. package/port/oracle/pi-acp/test/unit/session-delete.test.ts +170 -0
  124. package/port/oracle/pi-acp/test/unit/session-restore.test.ts +239 -0
  125. package/port/oracle/pi-acp/test/unit/slash-commands.test.ts +36 -0
  126. package/port/oracle/pi-acp/test/unit/startup-info-env.test.ts +84 -0
  127. package/port/oracle/pi-acp/test/unit/startup-info-load-session.test.ts +53 -0
  128. package/port/oracle/pi-acp/test/unit/startup-info-project-packages.test.ts +74 -0
  129. package/port/oracle/pi-acp/test/unit/stdout-destroyed-does-not-crash.test.ts +32 -0
  130. package/port/oracle/pi-acp/test/unit/thinking-level-rpc.test.ts +43 -0
  131. package/port/schema/LICENSE +191 -0
  132. package/port/schema/schema.json +8015 -0
  133. package/port/upstream-tests.json +201 -0
  134. package/provenance.json +28 -0
@@ -0,0 +1,530 @@
1
+ // Package pirpc drives `pig --mode rpc` as a child process (src/pi-rpc/* of pi-acp).
2
+ package pirpc
3
+
4
+ import (
5
+ "bufio"
6
+ "bytes"
7
+ "encoding/json"
8
+ "errors"
9
+ "fmt"
10
+ "io"
11
+ "os"
12
+ "os/exec"
13
+ "path/filepath"
14
+ "regexp"
15
+ "runtime"
16
+ "strings"
17
+ "sync"
18
+ "sync/atomic"
19
+ "time"
20
+ )
21
+
22
+ // SessionStatsTimeoutMs bounds the auxiliary context-usage request.
23
+ const SessionStatsTimeoutMs = 1000
24
+
25
+ // DefaultCommand is the executable started when none is configured.
26
+ func DefaultCommand(goos string) string {
27
+ if goos == "windows" {
28
+ return "pig.cmd"
29
+ }
30
+ return "pig"
31
+ }
32
+
33
+ // Command returns the override, or the default for this OS.
34
+ func Command(override string) string {
35
+ if override != "" {
36
+ return override
37
+ }
38
+ return DefaultCommand(runtime.GOOS)
39
+ }
40
+
41
+ // ShouldUseShell reports whether a launcher needs a shell (Windows .cmd and .bat).
42
+ func ShouldUseShell(goos, cmd string) bool {
43
+ if goos != "windows" {
44
+ return false
45
+ }
46
+ l := strings.ToLower(cmd)
47
+ return strings.HasSuffix(l, ".cmd") || strings.HasSuffix(l, ".bat")
48
+ }
49
+
50
+ // SpawnError is a failure to start the child (ENOENT, EACCES).
51
+ type SpawnError struct {
52
+ Message string
53
+ Code string
54
+ cause error
55
+ }
56
+
57
+ func (e *SpawnError) Error() string { return e.Message }
58
+ func (e *SpawnError) Unwrap() error { return e.cause }
59
+
60
+ // SpawnCode is the underlying error code (ENOENT, EACCES) or "".
61
+ func (e *SpawnError) SpawnCode() string { return e.Code }
62
+
63
+ // Response is a pi RPC response.
64
+ type Response struct {
65
+ Success bool
66
+ Data any
67
+ Error string
68
+ }
69
+
70
+ type wireResponse struct {
71
+ Type string `json:"type"`
72
+ ID *string `json:"id"`
73
+ Success bool `json:"success"`
74
+ Data json.RawMessage `json:"data"`
75
+ Error string `json:"error"`
76
+ }
77
+
78
+ type pendingCall struct {
79
+ ch chan result
80
+ }
81
+
82
+ type result struct {
83
+ res Response
84
+ err error
85
+ }
86
+
87
+ // Process is one running pig RPC child.
88
+ type Process struct {
89
+ stdin io.WriteCloser
90
+ kill func()
91
+
92
+ writeMu sync.Mutex
93
+
94
+ mu sync.Mutex
95
+ pending map[string]*pendingCall
96
+ handlers map[int]func(map[string]any)
97
+ nextH int
98
+ prelude []string
99
+ closed bool
100
+ exitErr error
101
+ seq atomic.Uint64
102
+
103
+ // requestFn replaces the transport in tests.
104
+ requestFn func(map[string]any, time.Duration) (Response, error)
105
+ }
106
+
107
+ // NewProcess builds a Process around already connected pipes (the tests' seam).
108
+ func NewProcess(stdin io.WriteCloser, stdout io.Reader, kill func()) *Process {
109
+ p := &Process{stdin: stdin, kill: kill, pending: map[string]*pendingCall{}, handlers: map[int]func(map[string]any){}}
110
+ go p.readLoop(stdout, nil)
111
+ return p
112
+ }
113
+
114
+ var ansiRE = regexp.MustCompile("[\x1b\u009b][\\[\\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]")
115
+
116
+ func (p *Process) readLoop(stdout io.Reader, wait func() error) {
117
+ br := bufio.NewReaderSize(stdout, 1<<20)
118
+ for {
119
+ line, err := readLine(br)
120
+ if t := bytes.TrimSpace(line); len(t) > 0 {
121
+ p.handleLine(t)
122
+ }
123
+ if err != nil {
124
+ break
125
+ }
126
+ }
127
+ code := "pi process exited"
128
+ if wait != nil {
129
+ if werr := wait(); werr != nil {
130
+ code = fmt.Sprintf("pi process exited (%v)", werr)
131
+ }
132
+ }
133
+ p.mu.Lock()
134
+ p.closed = true
135
+ p.exitErr = errors.New(code)
136
+ pend := p.pending
137
+ p.pending = map[string]*pendingCall{}
138
+ p.mu.Unlock()
139
+ for _, c := range pend {
140
+ c.ch <- result{err: p.exitErr}
141
+ }
142
+ }
143
+
144
+ func readLine(br *bufio.Reader) ([]byte, error) {
145
+ var out []byte
146
+ for {
147
+ part, err := br.ReadSlice('\n')
148
+ out = append(out, part...)
149
+ if err == bufio.ErrBufferFull {
150
+ continue
151
+ }
152
+ return out, err
153
+ }
154
+ }
155
+
156
+ func (p *Process) handleLine(line []byte) {
157
+ var msg map[string]any
158
+ if err := json.Unmarshal(line, &msg); err != nil {
159
+ // pig may print a human-readable prelude before the JSON lines start.
160
+ cleaned := strings.TrimRight(ansiRE.ReplaceAllString(string(line), ""), " \t\r\n")
161
+ if cleaned != "" {
162
+ p.mu.Lock()
163
+ p.prelude = append(p.prelude, cleaned)
164
+ p.mu.Unlock()
165
+ }
166
+ return
167
+ }
168
+ if msg["type"] == "response" {
169
+ // A response is never an event: unknown or timed-out ids are dropped.
170
+ id, ok := msg["id"].(string)
171
+ if !ok {
172
+ return
173
+ }
174
+ p.mu.Lock()
175
+ c := p.pending[id]
176
+ delete(p.pending, id)
177
+ p.mu.Unlock()
178
+ if c == nil {
179
+ return
180
+ }
181
+ var w wireResponse
182
+ _ = json.Unmarshal(line, &w)
183
+ r := Response{Success: w.Success, Error: w.Error}
184
+ if len(w.Data) > 0 {
185
+ _ = json.Unmarshal(w.Data, &r.Data)
186
+ }
187
+ c.ch <- result{res: r}
188
+ return
189
+ }
190
+ p.mu.Lock()
191
+ hs := make([]func(map[string]any), 0, len(p.handlers))
192
+ for i := 0; i < p.nextH; i++ {
193
+ if h, ok := p.handlers[i]; ok {
194
+ hs = append(hs, h)
195
+ }
196
+ }
197
+ p.mu.Unlock()
198
+ for _, h := range hs {
199
+ h(msg)
200
+ }
201
+ }
202
+
203
+ // Spawn starts the child.
204
+ func Spawn(cwd, command, sessionPath string) (*Process, error) {
205
+ return SpawnWithArgs(cwd, command, sessionPath, nil)
206
+ }
207
+
208
+ // SpawnWithArgs is Spawn with extra pig arguments (--piglet ...) placed before the session flag.
209
+ func SpawnWithArgs(cwd, command, sessionPath string, extra []string) (*Process, error) {
210
+ cmdName := Command(command)
211
+ cmd := exec.Command(cmdName, BuildArgs(extra, sessionPath)...)
212
+ cmd.Dir = cwd
213
+ cmd.Env = os.Environ()
214
+ cmd.Stderr = os.Stderr
215
+ stdin, err := cmd.StdinPipe()
216
+ if err != nil {
217
+ return nil, spawnFailure(cmdName, err)
218
+ }
219
+ stdout, err := cmd.StdoutPipe()
220
+ if err != nil {
221
+ return nil, spawnFailure(cmdName, err)
222
+ }
223
+ if err := cmd.Start(); err != nil {
224
+ return nil, spawnFailure(cmdName, err)
225
+ }
226
+ p := &Process{stdin: stdin, pending: map[string]*pendingCall{}, handlers: map[int]func(map[string]any){}}
227
+ p.kill = func() { terminate(cmd) }
228
+ go p.readLoop(stdout, cmd.Wait)
229
+
230
+ // Best-effort handshake. pig may report a sessionFile in a directory that is created lazily;
231
+ // create it now so later commands (export_html) do not fail on a missing parent.
232
+ if state, err := p.GetState(); err == nil {
233
+ if f, ok := state["sessionFile"].(string); ok && f != "" {
234
+ _ = os.MkdirAll(filepath.Dir(f), 0o755)
235
+ }
236
+ }
237
+ return p, nil
238
+ }
239
+
240
+ // BuildArgs is the pig argument list for RPC mode.
241
+ func BuildArgs(extra []string, sessionPath string) []string {
242
+ args := []string{"--mode", "rpc", "--no-themes"}
243
+ args = append(args, extra...)
244
+ if sessionPath != "" {
245
+ args = append(args, "--session", sessionPath)
246
+ }
247
+ return args
248
+ }
249
+
250
+ func spawnFailure(cmd string, err error) *SpawnError {
251
+ code := ""
252
+ switch {
253
+ case errors.Is(err, exec.ErrNotFound), errors.Is(err, os.ErrNotExist):
254
+ code = "ENOENT"
255
+ case errors.Is(err, os.ErrPermission):
256
+ code = "EACCES"
257
+ }
258
+ switch code {
259
+ case "ENOENT":
260
+ return &SpawnError{Code: code, cause: err, Message: fmt.Sprintf("Could not start pig: executable not found (command: %s). PiG needs to be installed before it can run in ACP clients. Install it from https://github.com/MichaelKinsy/PiG or ensure `pig` is on your PATH (or point --pig at a Piglet Binary). Then try again.", cmd)}
261
+ case "EACCES":
262
+ return &SpawnError{Code: code, cause: err, Message: fmt.Sprintf("Could not start pig: permission denied (command: %s).", cmd)}
263
+ }
264
+ return &SpawnError{Code: code, cause: err, Message: fmt.Sprintf("Could not start pig (command: %s).", cmd)}
265
+ }
266
+
267
+ // OnEvent registers an event handler.
268
+ func (p *Process) OnEvent(h func(map[string]any)) func() {
269
+ p.mu.Lock()
270
+ id := p.nextH
271
+ p.nextH++
272
+ p.handlers[id] = h
273
+ p.mu.Unlock()
274
+ return func() {
275
+ p.mu.Lock()
276
+ delete(p.handlers, id)
277
+ p.mu.Unlock()
278
+ }
279
+ }
280
+
281
+ // Dispose kills the child.
282
+ func (p *Process) Dispose() {
283
+ if p.kill != nil {
284
+ p.kill()
285
+ }
286
+ }
287
+
288
+ // PendingCount is the number of unanswered requests.
289
+ func (p *Process) PendingCount() int {
290
+ p.mu.Lock()
291
+ defer p.mu.Unlock()
292
+ return len(p.pending)
293
+ }
294
+
295
+ // ConsumePreludeLines returns, and clears, the human-readable lines pig printed before its JSON.
296
+ func (p *Process) ConsumePreludeLines() []string {
297
+ p.mu.Lock()
298
+ defer p.mu.Unlock()
299
+ out := p.prelude
300
+ p.prelude = nil
301
+ return out
302
+ }
303
+
304
+ // Request sends a raw command and waits for its response; timeout 0 waits without a limit.
305
+ func (p *Process) Request(cmd map[string]any, timeout time.Duration) (Response, error) {
306
+ if p.requestFn != nil {
307
+ return p.requestFn(cmd, timeout)
308
+ }
309
+ id := fmt.Sprintf("acp-%d-%d", os.Getpid(), p.seq.Add(1))
310
+ full := make(map[string]any, len(cmd)+1)
311
+ for k, v := range cmd {
312
+ full[k] = v
313
+ }
314
+ full["id"] = id
315
+ line, err := json.Marshal(full)
316
+ if err != nil {
317
+ return Response{}, err
318
+ }
319
+ call := &pendingCall{ch: make(chan result, 1)}
320
+ p.mu.Lock()
321
+ if p.closed {
322
+ err := p.exitErr
323
+ p.mu.Unlock()
324
+ return Response{}, err
325
+ }
326
+ p.pending[id] = call
327
+ p.mu.Unlock()
328
+
329
+ drop := func() bool {
330
+ p.mu.Lock()
331
+ defer p.mu.Unlock()
332
+ _, ok := p.pending[id]
333
+ delete(p.pending, id)
334
+ return ok
335
+ }
336
+ if err := p.writeLine(append(line, '\n')); err != nil {
337
+ if drop() {
338
+ return Response{}, err
339
+ }
340
+ }
341
+ var timer <-chan time.Time
342
+ if timeout > 0 {
343
+ t := time.NewTimer(timeout)
344
+ defer t.Stop()
345
+ timer = t.C
346
+ }
347
+ select {
348
+ case r := <-call.ch:
349
+ return r.res, r.err
350
+ case <-timer:
351
+ if !drop() {
352
+ r := <-call.ch
353
+ return r.res, r.err
354
+ }
355
+ return Response{}, fmt.Errorf("pi %v timed out after %dms", cmd["type"], timeout.Milliseconds())
356
+ }
357
+ }
358
+
359
+ func (p *Process) writeLine(b []byte) error {
360
+ p.writeMu.Lock()
361
+ defer p.writeMu.Unlock()
362
+ _, err := p.stdin.Write(b)
363
+ return err
364
+ }
365
+
366
+ func (p *Process) call(cmd map[string]any, timeout time.Duration) (Response, error) {
367
+ res, err := p.Request(cmd, timeout)
368
+ if err != nil {
369
+ return res, err
370
+ }
371
+ if !res.Success {
372
+ return res, fmt.Errorf("pi %v failed: %s", cmd["type"], failure(res))
373
+ }
374
+ return res, nil
375
+ }
376
+
377
+ func failure(res Response) string {
378
+ if res.Error != "" {
379
+ return res.Error
380
+ }
381
+ b, _ := json.Marshal(res.Data)
382
+ return string(b)
383
+ }
384
+
385
+ func asMap(v any) map[string]any {
386
+ m, _ := v.(map[string]any)
387
+ return m
388
+ }
389
+
390
+ // Prompt sends a prompt; the turn's events arrive through OnEvent.
391
+ func (p *Process) Prompt(message string, images []map[string]any) error {
392
+ if images == nil {
393
+ images = []map[string]any{}
394
+ }
395
+ _, err := p.call(map[string]any{"type": "prompt", "message": message, "images": images}, 0)
396
+ return err
397
+ }
398
+
399
+ // Abort stops the running turn.
400
+ func (p *Process) Abort() error { _, err := p.call(map[string]any{"type": "abort"}, 0); return err }
401
+
402
+ // GetState returns get_state data.
403
+ func (p *Process) GetState() (map[string]any, error) {
404
+ res, err := p.call(map[string]any{"type": "get_state"}, 0)
405
+ return asMap(res.Data), err
406
+ }
407
+
408
+ // GetAvailableModels returns get_available_models data.
409
+ func (p *Process) GetAvailableModels() (map[string]any, error) {
410
+ res, err := p.call(map[string]any{"type": "get_available_models"}, 0)
411
+ return asMap(res.Data), err
412
+ }
413
+
414
+ // SetModel switches the model.
415
+ func (p *Process) SetModel(provider, modelID string) error {
416
+ _, err := p.call(map[string]any{"type": "set_model", "provider": provider, "modelId": modelID}, 0)
417
+ return err
418
+ }
419
+
420
+ // GetAvailableThinkingLevels lists the levels the current model supports.
421
+ func (p *Process) GetAvailableThinkingLevels() ([]string, error) {
422
+ res, err := p.call(map[string]any{"type": "get_available_thinking_levels"}, 0)
423
+ if err != nil {
424
+ return nil, err
425
+ }
426
+ raw, _ := asMap(res.Data)["levels"].([]any)
427
+ if len(raw) == 0 {
428
+ return nil, errors.New("pi get_available_thinking_levels returned invalid levels")
429
+ }
430
+ levels := make([]string, 0, len(raw))
431
+ for _, l := range raw {
432
+ s, ok := l.(string)
433
+ if !ok || s == "" {
434
+ return nil, errors.New("pi get_available_thinking_levels returned invalid levels")
435
+ }
436
+ levels = append(levels, s)
437
+ }
438
+ return levels, nil
439
+ }
440
+
441
+ // SetThinkingLevel sets the thinking level.
442
+ func (p *Process) SetThinkingLevel(level string) error {
443
+ _, err := p.call(map[string]any{"type": "set_thinking_level", "level": level}, 0)
444
+ return err
445
+ }
446
+
447
+ // SetFollowUpMode sets how follow-up messages are delivered.
448
+ func (p *Process) SetFollowUpMode(mode string) error {
449
+ _, err := p.call(map[string]any{"type": "set_follow_up_mode", "mode": mode}, 0)
450
+ return err
451
+ }
452
+
453
+ // SetSteeringMode sets how steering messages are delivered.
454
+ func (p *Process) SetSteeringMode(mode string) error {
455
+ _, err := p.call(map[string]any{"type": "set_steering_mode", "mode": mode}, 0)
456
+ return err
457
+ }
458
+
459
+ // Compact runs manual compaction; customInstructions "" sends none.
460
+ func (p *Process) Compact(customInstructions string) (map[string]any, error) {
461
+ cmd := map[string]any{"type": "compact"}
462
+ if customInstructions != "" {
463
+ cmd["customInstructions"] = customInstructions
464
+ }
465
+ res, err := p.call(cmd, 0)
466
+ return asMap(res.Data), err
467
+ }
468
+
469
+ // SetAutoCompaction toggles automatic compaction.
470
+ func (p *Process) SetAutoCompaction(enabled bool) error {
471
+ _, err := p.call(map[string]any{"type": "set_auto_compaction", "enabled": enabled}, 0)
472
+ return err
473
+ }
474
+
475
+ // GetSessionStats asks for get_session_stats; timeoutMs 0 waits without a limit.
476
+ func (p *Process) GetSessionStats(timeoutMs int) (map[string]any, error) {
477
+ res, err := p.call(map[string]any{"type": "get_session_stats"}, time.Duration(timeoutMs)*time.Millisecond)
478
+ m := asMap(res.Data)
479
+ if m == nil && err == nil {
480
+ m = map[string]any{}
481
+ }
482
+ return m, err
483
+ }
484
+
485
+ // SetSessionName names the session.
486
+ func (p *Process) SetSessionName(name string) error {
487
+ _, err := p.call(map[string]any{"type": "set_session_name", "name": name}, 0)
488
+ return err
489
+ }
490
+
491
+ // ExportHTML returns the path pig wrote; outputPath "" lets pig choose.
492
+ func (p *Process) ExportHTML(outputPath string) (string, error) {
493
+ cmd := map[string]any{"type": "export_html"}
494
+ if outputPath != "" {
495
+ cmd["outputPath"] = outputPath
496
+ }
497
+ res, err := p.call(cmd, 0)
498
+ if err != nil {
499
+ return "", err
500
+ }
501
+ if s, ok := asMap(res.Data)["path"].(string); ok {
502
+ return s, nil
503
+ }
504
+ return "", nil
505
+ }
506
+
507
+ // GetMessages returns get_messages data.
508
+ func (p *Process) GetMessages() (map[string]any, error) {
509
+ res, err := p.call(map[string]any{"type": "get_messages"}, 0)
510
+ return asMap(res.Data), err
511
+ }
512
+
513
+ // GetCommands returns get_commands data.
514
+ func (p *Process) GetCommands() (map[string]any, error) {
515
+ res, err := p.call(map[string]any{"type": "get_commands"}, 0)
516
+ return asMap(res.Data), err
517
+ }
518
+
519
+ // SendExtensionUIResponse answers an extension_ui_request.
520
+ func (p *Process) SendExtensionUIResponse(resp map[string]any) error {
521
+ msg := map[string]any{"type": "extension_ui_response"}
522
+ for k, v := range resp {
523
+ msg[k] = v
524
+ }
525
+ b, err := json.Marshal(msg)
526
+ if err != nil {
527
+ return err
528
+ }
529
+ return p.writeLine(append(b, '\n'))
530
+ }