@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.
- package/CREDITS.md +23 -0
- package/LICENSE +22 -0
- package/README.md +110 -0
- package/extensions/acp/cmd/pig-acp/e2e_llm_test.go +168 -0
- package/extensions/acp/cmd/pig-acp/e2e_test.go +541 -0
- package/extensions/acp/cmd/pig-acp/go.mod +6 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent.go +784 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_builtin_test.go +536 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_commands_test.go +199 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_config_test.go +530 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_helpers_test.go +215 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_load_test.go +333 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_prompt.go +289 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_session_test.go +429 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/auth.go +61 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/bench_test.go +47 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/helpers_test.go +421 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/main_test.go +28 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/mutation_extra_test.go +142 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/paths.go +77 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/pisessions.go +317 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/protocol.go +188 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/server.go +218 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/server_test.go +386 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session.go +1068 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session_diff_test.go +183 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session_events_test.go +443 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session_extra_test.go +547 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session_usage_test.go +198 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/settings.go +79 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/slash.go +278 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/slash_test.go +128 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/startup.go +156 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/store.go +118 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/store_paths_test.go +466 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/translate.go +352 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/translate_test.go +105 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/twin_test.go +28 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/types.go +202 -0
- package/extensions/acp/cmd/pig-acp/internal/jsonrpc/bench_test.go +24 -0
- package/extensions/acp/cmd/pig-acp/internal/jsonrpc/jsonrpc.go +302 -0
- package/extensions/acp/cmd/pig-acp/internal/jsonrpc/jsonrpc_test.go +320 -0
- package/extensions/acp/cmd/pig-acp/internal/jsonrpc/twin_test.go +28 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/kill_unix.go +19 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/kill_windows.go +11 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/process.go +530 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/process_test.go +355 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/twin_test.go +28 -0
- package/extensions/acp/cmd/pig-acp/main.go +177 -0
- package/extensions/acp/cmd/pig-acp/main_test.go +213 -0
- package/extensions/acp/cmd/pig-acp/sigpipe_unix.go +10 -0
- package/extensions/acp/cmd/pig-acp/sigpipe_windows.go +5 -0
- package/extensions/acp/cmd/pig-acp/testdata/ask/extension.go +28 -0
- package/extensions/acp/cmd/pig-acp/testdata/ask/go.mod +7 -0
- package/extensions/acp/cmd/pig-acp/testdata/ask/go.sum +2 -0
- package/extensions/acp/cmd/pig-acp/testdata/gate/extension.go +37 -0
- package/extensions/acp/cmd/pig-acp/testdata/gate/go.mod +7 -0
- package/extensions/acp/cmd/pig-acp/testdata/gate/go.sum +2 -0
- package/extensions/acp/cmd/pig-acp/twin_test.go +28 -0
- package/extensions/acp/extension.go +63 -0
- package/extensions/acp/extension_test.go +111 -0
- package/extensions/acp/fakehost_test.go +548 -0
- package/extensions/acp/go.mod +6 -0
- package/extensions/acp/go.sum +2 -0
- package/package.json +41 -0
- package/port/PORT.md +147 -0
- package/port/interop/package-lock.json +89 -0
- package/port/interop/package.json +13 -0
- package/port/interop/run.mjs +229 -0
- package/port/mutate.mjs +110 -0
- package/port/mutation-results.txt +107 -0
- package/port/mutations.json +748 -0
- package/port/oracle/pi-acp/LICENSE +21 -0
- package/port/oracle/pi-acp/README.md +209 -0
- package/port/oracle/pi-acp/package.json +65 -0
- package/port/oracle/pi-acp/src/acp/agent.ts +1661 -0
- package/port/oracle/pi-acp/src/acp/auth-required.ts +37 -0
- package/port/oracle/pi-acp/src/acp/auth.ts +59 -0
- package/port/oracle/pi-acp/src/acp/paths.ts +15 -0
- package/port/oracle/pi-acp/src/acp/pi-commands.ts +59 -0
- package/port/oracle/pi-acp/src/acp/pi-sessions.ts +333 -0
- package/port/oracle/pi-acp/src/acp/pi-settings.ts +75 -0
- package/port/oracle/pi-acp/src/acp/session-store.ts +68 -0
- package/port/oracle/pi-acp/src/acp/session.ts +1078 -0
- package/port/oracle/pi-acp/src/acp/slash-commands.ts +197 -0
- package/port/oracle/pi-acp/src/acp/translate/bash.ts +100 -0
- package/port/oracle/pi-acp/src/acp/translate/pi-messages.ts +17 -0
- package/port/oracle/pi-acp/src/acp/translate/pi-tools.ts +51 -0
- package/port/oracle/pi-acp/src/acp/translate/prompt.ts +71 -0
- package/port/oracle/pi-acp/src/index.ts +82 -0
- package/port/oracle/pi-acp/src/pi-rpc/command.ts +16 -0
- package/port/oracle/pi-acp/src/pi-rpc/process.ts +432 -0
- package/port/oracle/pi-acp/test/component/agent-steering-followup-modes.test.ts +143 -0
- package/port/oracle/pi-acp/test/component/session-diff.test.ts +214 -0
- package/port/oracle/pi-acp/test/component/session-events.test.ts +1102 -0
- package/port/oracle/pi-acp/test/component/session-list-and-load.test.ts +129 -0
- package/port/oracle/pi-acp/test/component/session-list-custom-session-dir.test.ts +52 -0
- package/port/oracle/pi-acp/test/component/session-list-scoped.test.ts +76 -0
- package/port/oracle/pi-acp/test/component/session-load-toolresult.test.ts +68 -0
- package/port/oracle/pi-acp/test/component/session-queue-cancel.test.ts +43 -0
- package/port/oracle/pi-acp/test/component/session-slash-commands.test.ts +37 -0
- package/port/oracle/pi-acp/test/component/session-thinking-modes.test.ts +22 -0
- package/port/oracle/pi-acp/test/component/session-title-long-session.test.ts +56 -0
- package/port/oracle/pi-acp/test/component/session-updatedAt-message-only.test.ts +57 -0
- package/port/oracle/pi-acp/test/helpers/fakes.ts +87 -0
- package/port/oracle/pi-acp/test/unit/auth-methods-terminal-auth-meta.test.ts +22 -0
- package/port/oracle/pi-acp/test/unit/builtin-commands.test.ts +60 -0
- package/port/oracle/pi-acp/test/unit/context-usage.test.ts +204 -0
- package/port/oracle/pi-acp/test/unit/merge-commands.test.ts +19 -0
- package/port/oracle/pi-acp/test/unit/model-thinking-levels.test.ts +268 -0
- package/port/oracle/pi-acp/test/unit/new-session-auth-required-when-no-models.test.ts +51 -0
- package/port/oracle/pi-acp/test/unit/new-session-pi-not-found.test.ts +26 -0
- package/port/oracle/pi-acp/test/unit/new-session-runtime-startup-errors.test.ts +148 -0
- package/port/oracle/pi-acp/test/unit/pi-command.test.ts +44 -0
- package/port/oracle/pi-acp/test/unit/pi-commands.test.ts +32 -0
- package/port/oracle/pi-acp/test/unit/pi-enable-embed-context-flag.test.ts +35 -0
- package/port/oracle/pi-acp/test/unit/pi-messages.test.ts +29 -0
- package/port/oracle/pi-acp/test/unit/pi-rpc-request-timeout.test.ts +137 -0
- package/port/oracle/pi-acp/test/unit/pi-rpc-session-path.test.ts +93 -0
- package/port/oracle/pi-acp/test/unit/pi-tools.test.ts +40 -0
- package/port/oracle/pi-acp/test/unit/prompt-to-pi-message.test.ts +74 -0
- package/port/oracle/pi-acp/test/unit/session-config-options.test.ts +217 -0
- package/port/oracle/pi-acp/test/unit/session-delete.test.ts +170 -0
- package/port/oracle/pi-acp/test/unit/session-restore.test.ts +239 -0
- package/port/oracle/pi-acp/test/unit/slash-commands.test.ts +36 -0
- package/port/oracle/pi-acp/test/unit/startup-info-env.test.ts +84 -0
- package/port/oracle/pi-acp/test/unit/startup-info-load-session.test.ts +53 -0
- package/port/oracle/pi-acp/test/unit/startup-info-project-packages.test.ts +74 -0
- package/port/oracle/pi-acp/test/unit/stdout-destroyed-does-not-crash.test.ts +32 -0
- package/port/oracle/pi-acp/test/unit/thinking-level-rpc.test.ts +43 -0
- package/port/schema/LICENSE +191 -0
- package/port/schema/schema.json +8015 -0
- package/port/upstream-tests.json +201 -0
- 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
|
+
}
|