@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,352 @@
|
|
|
1
|
+
package acp
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"strings"
|
|
8
|
+
"unicode"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// This file ports src/acp/translate/{prompt,pi-tools,pi-messages,bash}.ts. Values that come
|
|
12
|
+
// from pi are decoded JSON (map[string]any, []any, string, float64), read the way the original
|
|
13
|
+
// reads them: a wrong type reads as absent, never as an error.
|
|
14
|
+
|
|
15
|
+
// jsTrim is String.prototype.trim: Unicode white space plus the byte order mark.
|
|
16
|
+
func jsTrim(s string) string {
|
|
17
|
+
return strings.TrimFunc(s, func(r rune) bool { return unicode.IsSpace(r) || r == '\uFEFF' })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func asObject(v any) map[string]any {
|
|
21
|
+
m, _ := v.(map[string]any)
|
|
22
|
+
return m
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func strOf(m map[string]any, key string) (string, bool) {
|
|
26
|
+
s, ok := m[key].(string)
|
|
27
|
+
return s, ok
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// base64ByteLength is Buffer.byteLength(s, 'base64').
|
|
31
|
+
func base64ByteLength(s string) int {
|
|
32
|
+
n := len(s)
|
|
33
|
+
if n == 0 {
|
|
34
|
+
return 0
|
|
35
|
+
}
|
|
36
|
+
pad := 0
|
|
37
|
+
if strings.HasSuffix(s, "==") {
|
|
38
|
+
pad = 2
|
|
39
|
+
} else if strings.HasSuffix(s, "=") {
|
|
40
|
+
pad = 1
|
|
41
|
+
}
|
|
42
|
+
return (n*3)>>2 - pad
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// PromptToPiMessage turns ACP prompt blocks into pi message text and images.
|
|
46
|
+
func PromptToPiMessage(blocks []ContentBlock) (string, []Image) {
|
|
47
|
+
var msg strings.Builder
|
|
48
|
+
images := []Image{}
|
|
49
|
+
for _, b := range blocks {
|
|
50
|
+
switch b["type"] {
|
|
51
|
+
case "text":
|
|
52
|
+
if s, ok := b["text"].(string); ok {
|
|
53
|
+
msg.WriteString(s)
|
|
54
|
+
}
|
|
55
|
+
case "resource_link":
|
|
56
|
+
fmt.Fprintf(&msg, "\n[Context] %s", jsString(b["uri"]))
|
|
57
|
+
case "image":
|
|
58
|
+
// pi expects base64 image bytes in data without a data-url prefix.
|
|
59
|
+
images = append(images, Image{"type": "image", "mimeType": b["mimeType"], "data": b["data"]})
|
|
60
|
+
case "resource":
|
|
61
|
+
r := asObject(b["resource"])
|
|
62
|
+
uri := "(unknown)"
|
|
63
|
+
if s, ok := strOf(r, "uri"); ok {
|
|
64
|
+
uri = s
|
|
65
|
+
}
|
|
66
|
+
if text, ok := strOf(r, "text"); ok {
|
|
67
|
+
mime := "text/plain"
|
|
68
|
+
if s, ok := strOf(r, "mimeType"); ok {
|
|
69
|
+
mime = s
|
|
70
|
+
}
|
|
71
|
+
fmt.Fprintf(&msg, "\n[Embedded Context] %s (%s)\n%s", uri, mime, text)
|
|
72
|
+
} else if blob, ok := strOf(r, "blob"); ok {
|
|
73
|
+
mime := "application/octet-stream"
|
|
74
|
+
if s, ok := strOf(r, "mimeType"); ok {
|
|
75
|
+
mime = s
|
|
76
|
+
}
|
|
77
|
+
fmt.Fprintf(&msg, "\n[Embedded Context] %s (%s, %d bytes)", uri, mime, base64ByteLength(blob))
|
|
78
|
+
} else {
|
|
79
|
+
fmt.Fprintf(&msg, "\n[Embedded Context] %s", uri)
|
|
80
|
+
}
|
|
81
|
+
case "audio":
|
|
82
|
+
data, _ := b["data"].(string)
|
|
83
|
+
fmt.Fprintf(&msg, "\n[Audio] (%s, %d bytes) not supported by pig-acp", jsString(b["mimeType"]), base64ByteLength(data))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return msg.String(), images
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// jsString renders a value like a JS template literal would.
|
|
90
|
+
func jsString(v any) string {
|
|
91
|
+
switch x := v.(type) {
|
|
92
|
+
case nil:
|
|
93
|
+
return "undefined"
|
|
94
|
+
case string:
|
|
95
|
+
return x
|
|
96
|
+
case float64:
|
|
97
|
+
return fmt.Sprint(x)
|
|
98
|
+
default:
|
|
99
|
+
return fmt.Sprint(x)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func textBlocks(content any) string {
|
|
104
|
+
arr, ok := content.([]any)
|
|
105
|
+
if !ok {
|
|
106
|
+
return ""
|
|
107
|
+
}
|
|
108
|
+
var b strings.Builder
|
|
109
|
+
for _, c := range arr {
|
|
110
|
+
m := asObject(c)
|
|
111
|
+
if m["type"] == "text" {
|
|
112
|
+
if s, ok := m["text"].(string); ok {
|
|
113
|
+
b.WriteString(s)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return b.String()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// NormalizePiMessageText joins the text of a user message.
|
|
121
|
+
func NormalizePiMessageText(content any) string {
|
|
122
|
+
if s, ok := content.(string); ok {
|
|
123
|
+
return s
|
|
124
|
+
}
|
|
125
|
+
return textBlocks(content)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// NormalizePiAssistantText joins the text blocks of an assistant message.
|
|
129
|
+
func NormalizePiAssistantText(content any) string { return textBlocks(content) }
|
|
130
|
+
|
|
131
|
+
func firstString(pairs ...[2]any) (string, bool) {
|
|
132
|
+
for _, p := range pairs {
|
|
133
|
+
if m, ok := p[0].(map[string]any); ok {
|
|
134
|
+
if s, ok := m[p[1].(string)].(string); ok {
|
|
135
|
+
return s, true
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return "", false
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// asNumber reads a JSON number (float64 after decoding; Go integers when built by hand).
|
|
143
|
+
func asNumber(v any) (float64, bool) {
|
|
144
|
+
switch n := v.(type) {
|
|
145
|
+
case float64:
|
|
146
|
+
return n, true
|
|
147
|
+
case float32:
|
|
148
|
+
return float64(n), true
|
|
149
|
+
case int:
|
|
150
|
+
return float64(n), true
|
|
151
|
+
case int64:
|
|
152
|
+
return float64(n), true
|
|
153
|
+
case int32:
|
|
154
|
+
return float64(n), true
|
|
155
|
+
}
|
|
156
|
+
return 0, false
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
func firstNumber(pairs ...[2]any) (float64, bool) {
|
|
160
|
+
for _, p := range pairs {
|
|
161
|
+
if m, ok := p[0].(map[string]any); ok {
|
|
162
|
+
if n, ok := asNumber(m[p[1].(string)]); ok {
|
|
163
|
+
return n, true
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return 0, false
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// jsonStringify is JSON.stringify(v, null, 2) apart from key order (Go sorts map keys).
|
|
171
|
+
func jsonStringify(v any) string {
|
|
172
|
+
var buf bytes.Buffer
|
|
173
|
+
enc := json.NewEncoder(&buf)
|
|
174
|
+
enc.SetEscapeHTML(false)
|
|
175
|
+
enc.SetIndent("", " ")
|
|
176
|
+
if err := enc.Encode(v); err != nil {
|
|
177
|
+
return fmt.Sprint(v)
|
|
178
|
+
}
|
|
179
|
+
return strings.TrimSuffix(buf.String(), "\n")
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// truthy is JS truthiness for the values a tool result can hold.
|
|
183
|
+
func truthy(v any) bool {
|
|
184
|
+
switch x := v.(type) {
|
|
185
|
+
case nil:
|
|
186
|
+
return false
|
|
187
|
+
case bool:
|
|
188
|
+
return x
|
|
189
|
+
case string:
|
|
190
|
+
return x != ""
|
|
191
|
+
case float64:
|
|
192
|
+
return x != 0
|
|
193
|
+
}
|
|
194
|
+
return true
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ToolResultToText renders a pi tool result as text.
|
|
198
|
+
func ToolResultToText(result any) string {
|
|
199
|
+
if !truthy(result) {
|
|
200
|
+
return ""
|
|
201
|
+
}
|
|
202
|
+
res := asObject(result)
|
|
203
|
+
details := asObject(res["details"])
|
|
204
|
+
|
|
205
|
+
// pi's edit tool returns a terse success message in content and the full unified diff in details.diff.
|
|
206
|
+
if diff, ok := details["diff"].(string); ok && jsTrim(diff) != "" {
|
|
207
|
+
return diff
|
|
208
|
+
}
|
|
209
|
+
if content, ok := res["content"].([]any); ok {
|
|
210
|
+
var texts []string
|
|
211
|
+
for _, c := range content {
|
|
212
|
+
m := asObject(c)
|
|
213
|
+
if s, ok := m["text"].(string); ok && m["type"] == "text" && s != "" {
|
|
214
|
+
texts = append(texts, s)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if len(texts) > 0 {
|
|
218
|
+
return strings.Join(texts, "")
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
stdout, hasOut := firstString([2]any{details, "stdout"}, [2]any{res, "stdout"}, [2]any{details, "output"}, [2]any{res, "output"})
|
|
223
|
+
stderr, hasErr := firstString([2]any{details, "stderr"}, [2]any{res, "stderr"})
|
|
224
|
+
exit, hasExit := firstNumber([2]any{details, "exitCode"}, [2]any{res, "exitCode"}, [2]any{details, "code"}, [2]any{res, "code"})
|
|
225
|
+
|
|
226
|
+
if (hasOut && jsTrim(stdout) != "") || (hasErr && jsTrim(stderr) != "") {
|
|
227
|
+
var parts []string
|
|
228
|
+
if hasOut && jsTrim(stdout) != "" {
|
|
229
|
+
parts = append(parts, stdout)
|
|
230
|
+
}
|
|
231
|
+
if hasErr && jsTrim(stderr) != "" {
|
|
232
|
+
parts = append(parts, "stderr:\n"+stderr)
|
|
233
|
+
}
|
|
234
|
+
if hasExit {
|
|
235
|
+
parts = append(parts, fmt.Sprintf("exit code: %s", jsNumber(exit)))
|
|
236
|
+
}
|
|
237
|
+
return strings.TrimRightFunc(strings.Join(parts, "\n\n"), func(r rune) bool { return unicode.IsSpace(r) || r == '\uFEFF' })
|
|
238
|
+
}
|
|
239
|
+
return jsonStringify(result)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
func jsNumber(f float64) string {
|
|
243
|
+
if f == float64(int64(f)) {
|
|
244
|
+
return fmt.Sprint(int64(f))
|
|
245
|
+
}
|
|
246
|
+
return fmt.Sprint(f)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// IsBashTool reports whether a tool name is bash (case-insensitive).
|
|
250
|
+
func IsBashTool(name string) bool { return strings.ToLower(name) == "bash" }
|
|
251
|
+
|
|
252
|
+
// BashCommand finds the command in tool arguments or a result.
|
|
253
|
+
func BashCommand(v any) (string, bool) {
|
|
254
|
+
rec := asObject(v)
|
|
255
|
+
get := func(m map[string]any) (any, bool) {
|
|
256
|
+
if m == nil {
|
|
257
|
+
return nil, false
|
|
258
|
+
}
|
|
259
|
+
for _, k := range []string{"command", "cmd"} {
|
|
260
|
+
if x, ok := m[k]; ok && x != nil {
|
|
261
|
+
return x, true
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return nil, false
|
|
265
|
+
}
|
|
266
|
+
// ?? picks the first non-nullish value even when it is not a string.
|
|
267
|
+
for _, m := range []map[string]any{rec, asObject(rec["args"]), asObject(rec["input"]), asObject(rec["rawInput"]), asObject(rec["toolInput"]), asObject(rec["details"])} {
|
|
268
|
+
if x, ok := get(m); ok {
|
|
269
|
+
if s, ok := x.(string); ok && jsTrim(s) != "" {
|
|
270
|
+
return s, true
|
|
271
|
+
}
|
|
272
|
+
return "", false
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return "", false
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// BashResultText is the output text of a bash result.
|
|
279
|
+
func BashResultText(result any) string {
|
|
280
|
+
rec := asObject(result)
|
|
281
|
+
if content, ok := rec["content"].([]any); ok {
|
|
282
|
+
var texts []string
|
|
283
|
+
for _, c := range content {
|
|
284
|
+
m := asObject(c)
|
|
285
|
+
if s, ok := m["text"].(string); ok && m["type"] == "text" && s != "" {
|
|
286
|
+
texts = append(texts, s)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if len(texts) > 0 {
|
|
290
|
+
return strings.Join(texts, "")
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
details := asObject(rec["details"])
|
|
294
|
+
stdout, _ := firstString([2]any{details, "stdout"}, [2]any{rec, "stdout"}, [2]any{details, "output"}, [2]any{rec, "output"})
|
|
295
|
+
stderr, _ := firstString([2]any{details, "stderr"}, [2]any{rec, "stderr"})
|
|
296
|
+
var parts []string
|
|
297
|
+
for _, p := range []string{stdout, stderr} {
|
|
298
|
+
if p != "" {
|
|
299
|
+
parts = append(parts, p)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return strings.Join(parts, "\n")
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// BashExitCode is the exit code of a bash result (1 for an error without one).
|
|
306
|
+
func BashExitCode(result any, isError bool) int {
|
|
307
|
+
rec := asObject(result)
|
|
308
|
+
details := asObject(rec["details"])
|
|
309
|
+
// details?.exitCode ?? record?.exitCode ?? details?.code ?? record?.code: first non-nullish.
|
|
310
|
+
for _, p := range [][2]any{{details, "exitCode"}, {rec, "exitCode"}, {details, "code"}, {rec, "code"}} {
|
|
311
|
+
if m, _ := p[0].(map[string]any); m != nil {
|
|
312
|
+
if v, ok := m[p[1].(string)]; ok && v != nil {
|
|
313
|
+
if n, ok := asNumber(v); ok {
|
|
314
|
+
return int(n)
|
|
315
|
+
}
|
|
316
|
+
break
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if isError {
|
|
321
|
+
return 1
|
|
322
|
+
}
|
|
323
|
+
return 0
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// BashOutputDelta is the appended part of the output.
|
|
327
|
+
func BashOutputDelta(previous, next string) string {
|
|
328
|
+
if strings.HasPrefix(next, previous) {
|
|
329
|
+
return next[len(previous):]
|
|
330
|
+
}
|
|
331
|
+
return next
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// BashTerminalContent is the terminal tool-call content.
|
|
335
|
+
func BashTerminalContent(toolCallID string) []any {
|
|
336
|
+
return []any{map[string]any{"type": "terminal", "terminalId": toolCallID}}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// BashTerminalInfoMeta is the terminal_info _meta.
|
|
340
|
+
func BashTerminalInfoMeta(toolCallID, cwd string) map[string]any {
|
|
341
|
+
return map[string]any{"terminal_info": map[string]any{"terminal_id": toolCallID, "cwd": cwd}}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// BashTerminalOutputMeta is the terminal_output _meta.
|
|
345
|
+
func BashTerminalOutputMeta(toolCallID, data string) map[string]any {
|
|
346
|
+
return map[string]any{"terminal_output": map[string]any{"terminal_id": toolCallID, "data": data}}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// BashTerminalExitMeta is the terminal_exit _meta.
|
|
350
|
+
func BashTerminalExitMeta(toolCallID string, exitCode int) map[string]any {
|
|
351
|
+
return map[string]any{"terminal_exit": map[string]any{"terminal_id": toolCallID, "exit_code": exitCode, "signal": nil}}
|
|
352
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
package acp
|
|
2
|
+
|
|
3
|
+
// Twins of test/unit/pi-tools.test.ts, prompt-to-pi-message.test.ts, pi-messages.test.ts.
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"encoding/base64"
|
|
7
|
+
"regexp"
|
|
8
|
+
"strings"
|
|
9
|
+
"testing"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func TestPiTools(t *testing.T) {
|
|
13
|
+
tw(t, "unit/pi-tools", "toolResultToText: extracts text from content blocks", func(t *testing.T) {
|
|
14
|
+
got := ToolResultToText(map[string]any{"content": []any{
|
|
15
|
+
map[string]any{"type": "text", "text": "hello"}, map[string]any{"type": "text", "text": " world"}}})
|
|
16
|
+
if got != "hello world" {
|
|
17
|
+
t.Errorf("got %q", got)
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
tw(t, "unit/pi-tools", "toolResultToText: prefers details.diff when present", func(t *testing.T) {
|
|
21
|
+
got := ToolResultToText(map[string]any{
|
|
22
|
+
"content": []any{map[string]any{"type": "text", "text": "Successfully replaced 2 block(s) in a.txt."}},
|
|
23
|
+
"details": map[string]any{"diff": "--- a\n+++ b\n"}})
|
|
24
|
+
if got != "--- a\n+++ b\n" {
|
|
25
|
+
t.Errorf("got %q", got)
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
tw(t, "unit/pi-tools", "toolResultToText: falls back to JSON", func(t *testing.T) {
|
|
29
|
+
got := ToolResultToText(map[string]any{"a": 1})
|
|
30
|
+
if !regexp.MustCompile(`"a": 1`).MatchString(got) {
|
|
31
|
+
t.Errorf("got %q", got)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
tw(t, "unit/pi-tools", "toolResultToText: extracts bash stdout/stderr from details", func(t *testing.T) {
|
|
35
|
+
got := ToolResultToText(map[string]any{"details": map[string]any{"stdout": "ok\n", "stderr": "warn\n", "exitCode": 0}})
|
|
36
|
+
for _, want := range []string{"ok", "stderr:", "warn", "exit code: 0"} {
|
|
37
|
+
if !strings.Contains(got, want) {
|
|
38
|
+
t.Errorf("%q missing %q", got, want)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func TestPromptToPiMessage(t *testing.T) {
|
|
45
|
+
tw(t, "unit/prompt-to-pi-message", "promptToPiMessage: concatenates text and resource links", func(t *testing.T) {
|
|
46
|
+
msg, images := PromptToPiMessage([]ContentBlock{
|
|
47
|
+
{"type": "text", "text": "Hello"},
|
|
48
|
+
{"type": "resource_link", "uri": "file:///tmp/foo.txt", "name": "foo"},
|
|
49
|
+
{"type": "text", "text": " world"}})
|
|
50
|
+
if msg != "Hello\n[Context] file:///tmp/foo.txt world" || len(images) != 0 {
|
|
51
|
+
t.Errorf("msg=%q images=%v", msg, images)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
tw(t, "unit/prompt-to-pi-message", "promptToPiMessage: includes embedded resource text as marker", func(t *testing.T) {
|
|
55
|
+
msg, images := PromptToPiMessage([]ContentBlock{{"type": "resource", "resource": map[string]any{"uri": "file:///tmp/a.txt", "mimeType": "text/plain", "text": "hi"}}})
|
|
56
|
+
if msg != "\n[Embedded Context] file:///tmp/a.txt (text/plain)\nhi" || len(images) != 0 {
|
|
57
|
+
t.Errorf("msg=%q", msg)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
tw(t, "unit/prompt-to-pi-message", "promptToPiMessage: includes embedded resource blob as marker", func(t *testing.T) {
|
|
61
|
+
blob := base64.StdEncoding.EncodeToString([]byte("xyz"))
|
|
62
|
+
msg, images := PromptToPiMessage([]ContentBlock{{"type": "resource", "resource": map[string]any{"uri": "file:///tmp/a.bin", "mimeType": "application/octet-stream", "blob": blob}}})
|
|
63
|
+
if msg != "\n[Embedded Context] file:///tmp/a.bin (application/octet-stream, 3 bytes)" || len(images) != 0 {
|
|
64
|
+
t.Errorf("msg=%q", msg)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
tw(t, "unit/prompt-to-pi-message", "promptToPiMessage: includes audio as marker", func(t *testing.T) {
|
|
68
|
+
data := base64.StdEncoding.EncodeToString([]byte("abc"))
|
|
69
|
+
msg, images := PromptToPiMessage([]ContentBlock{{"type": "audio", "mimeType": "audio/wav", "data": data}})
|
|
70
|
+
// Renamed identity: the marker names pig-acp, where the original names pi-acp.
|
|
71
|
+
if msg != "\n[Audio] (audio/wav, 3 bytes) not supported by pig-acp" || len(images) != 0 {
|
|
72
|
+
t.Errorf("msg=%q", msg)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
tw(t, "unit/prompt-to-pi-message", "promptToPiMessage: maps image to pi image content", func(t *testing.T) {
|
|
76
|
+
b64 := base64.StdEncoding.EncodeToString([]byte("abc"))
|
|
77
|
+
msg, images := PromptToPiMessage([]ContentBlock{{"type": "text", "text": "see"}, {"type": "image", "mimeType": "image/png", "data": b64, "uri": "img-1"}})
|
|
78
|
+
if msg != "see" || len(images) != 1 {
|
|
79
|
+
t.Fatalf("msg=%q images=%v", msg, images)
|
|
80
|
+
}
|
|
81
|
+
jsonEqual(t, images[0], map[string]any{"type": "image", "mimeType": "image/png", "data": b64})
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func TestPiMessages(t *testing.T) {
|
|
86
|
+
tw(t, "unit/pi-messages", "normalizePiMessageText: supports string", func(t *testing.T) {
|
|
87
|
+
if NormalizePiMessageText("hello") != "hello" {
|
|
88
|
+
t.Fail()
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
tw(t, "unit/pi-messages", "normalizePiMessageText: joins text blocks", func(t *testing.T) {
|
|
92
|
+
got := NormalizePiMessageText([]any{
|
|
93
|
+
map[string]any{"type": "text", "text": "a"}, map[string]any{"type": "text", "text": "b"}, map[string]any{"type": "not_text", "x": 1}})
|
|
94
|
+
if got != "ab" {
|
|
95
|
+
t.Errorf("got %q", got)
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
tw(t, "unit/pi-messages", "normalizePiAssistantText: joins only text blocks", func(t *testing.T) {
|
|
99
|
+
got := NormalizePiAssistantText([]any{
|
|
100
|
+
map[string]any{"type": "text", "text": "hi"}, map[string]any{"type": "thinking", "text": "..."}, map[string]any{"type": "text", "text": "!"}})
|
|
101
|
+
if got != "hi!" {
|
|
102
|
+
t.Errorf("got %q", got)
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package acp
|
|
2
|
+
|
|
3
|
+
// Twin helpers for a port whose original has tests. Every upstream test case gets one
|
|
4
|
+
// tw(...) call carrying the upstream title unchanged, or one tskip(...) with the reason it
|
|
5
|
+
// cannot be ported. `pigeq twins check` reads these calls and fails on a missing title, an
|
|
6
|
+
// unknown one (a misspelling) or one reason shared by many skips.
|
|
7
|
+
//
|
|
8
|
+
// pigeq twins list --tests <upstream>/test > port/upstream-tests.json
|
|
9
|
+
// pigeq twins check --ledger port/upstream-tests.json --go extensions/<name> [--files a,b]
|
|
10
|
+
//
|
|
11
|
+
// Copy this file into the port as twin_test.go and replace acp with the package name (in
|
|
12
|
+
// an external test package use `acp_test`). A parametrized upstream title (it contains
|
|
13
|
+
// `${`) is twinned once, with the template text unchanged.
|
|
14
|
+
|
|
15
|
+
import "testing"
|
|
16
|
+
|
|
17
|
+
// tw runs one exact twin: file is the upstream test file (its name without .test.mjs), title
|
|
18
|
+
// the upstream test title, unchanged.
|
|
19
|
+
func tw(t *testing.T, file, title string, fn func(t *testing.T)) {
|
|
20
|
+
t.Helper()
|
|
21
|
+
t.Run(title, fn)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// tskip records a named gap: the upstream case has no Go twin, with its own reason.
|
|
25
|
+
func tskip(t *testing.T, file, title, reason string) {
|
|
26
|
+
t.Helper()
|
|
27
|
+
t.Run(title, func(t *testing.T) { t.Skip(reason) })
|
|
28
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Package acp is the Agent Client Protocol side of pig-acp: the translation layer between
|
|
2
|
+
// an ACP client (an editor) and a PiG agent driven over `pig --mode rpc`. It ports the
|
|
3
|
+
// src/acp/* tree of pi-acp (Sergii Kozak, MIT); see the Package's CREDITS.md.
|
|
4
|
+
package acp
|
|
5
|
+
|
|
6
|
+
import "fmt"
|
|
7
|
+
|
|
8
|
+
// ProtocolVersion is the only ACP protocol version this adapter speaks. It is pinned to the
|
|
9
|
+
// schema of @agentclientprotocol/sdk 0.26.0 (PROTOCOL_VERSION = 1), the one pi-acp builds on.
|
|
10
|
+
const ProtocolVersion = 1
|
|
11
|
+
|
|
12
|
+
// Update is one ACP session update (`session/update` params.update). It is a JSON object,
|
|
13
|
+
// so an omitted field and a null field stay distinct, as in the original.
|
|
14
|
+
type Update = map[string]any
|
|
15
|
+
|
|
16
|
+
// Event is one record pig writes in RPC mode (an agent event or an extension UI request).
|
|
17
|
+
type Event = map[string]any
|
|
18
|
+
|
|
19
|
+
// ContentBlock is one block of an ACP prompt.
|
|
20
|
+
type ContentBlock = map[string]any
|
|
21
|
+
|
|
22
|
+
// StopReason is how a prompt turn ended. "error" is internal: ACP has no such stop reason.
|
|
23
|
+
type StopReason string
|
|
24
|
+
|
|
25
|
+
// Stop reasons.
|
|
26
|
+
const (
|
|
27
|
+
StopEndTurn StopReason = "end_turn"
|
|
28
|
+
StopCancelled StopReason = "cancelled"
|
|
29
|
+
StopError StopReason = "error"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// RequestError is a JSON-RPC error with an ACP code (the SDK's RequestError).
|
|
33
|
+
type RequestError struct {
|
|
34
|
+
Code int
|
|
35
|
+
Message string
|
|
36
|
+
Data any
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func (e *RequestError) Error() string { return e.Message }
|
|
40
|
+
|
|
41
|
+
func withDetail(base, detail string) string {
|
|
42
|
+
if detail == "" {
|
|
43
|
+
return base
|
|
44
|
+
}
|
|
45
|
+
return fmt.Sprintf("%s: %s", base, detail)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ErrParse is JSON-RPC -32700.
|
|
49
|
+
func ErrParse(data any, detail string) *RequestError {
|
|
50
|
+
return &RequestError{Code: -32700, Message: withDetail("Parse error", detail), Data: data}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ErrInvalidRequest is JSON-RPC -32600.
|
|
54
|
+
func ErrInvalidRequest(data any, detail string) *RequestError {
|
|
55
|
+
return &RequestError{Code: -32600, Message: withDetail("Invalid request", detail), Data: data}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ErrMethodNotFound is JSON-RPC -32601.
|
|
59
|
+
func ErrMethodNotFound(method string) *RequestError {
|
|
60
|
+
return &RequestError{Code: -32601, Message: fmt.Sprintf("%q: %s", "Method not found", method), Data: map[string]any{"method": method}}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ErrInvalidParams is JSON-RPC -32602.
|
|
64
|
+
func ErrInvalidParams(data any, detail string) *RequestError {
|
|
65
|
+
return &RequestError{Code: -32602, Message: withDetail("Invalid params", detail), Data: data}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ErrInternal is JSON-RPC -32603.
|
|
69
|
+
func ErrInternal(data any, detail string) *RequestError {
|
|
70
|
+
return &RequestError{Code: -32603, Message: withDetail("Internal error", detail), Data: data}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ErrAuthRequired is the ACP "authentication required" error, -32000.
|
|
74
|
+
func ErrAuthRequired(data any, detail string) *RequestError {
|
|
75
|
+
return &RequestError{Code: -32000, Message: withDetail("Authentication required", detail), Data: data}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// PermissionOption is one choice offered in `session/request_permission`.
|
|
79
|
+
type PermissionOption struct {
|
|
80
|
+
OptionID string `json:"optionId"`
|
|
81
|
+
Name string `json:"name"`
|
|
82
|
+
Kind string `json:"kind"`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// PermissionRequest is `session/request_permission` params.
|
|
86
|
+
type PermissionRequest struct {
|
|
87
|
+
SessionID string `json:"sessionId"`
|
|
88
|
+
ToolCall map[string]any `json:"toolCall"`
|
|
89
|
+
Options []PermissionOption `json:"options"`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// PermissionOutcome is the client's answer: Outcome "selected" (with OptionID) or "cancelled".
|
|
93
|
+
type PermissionOutcome struct {
|
|
94
|
+
Outcome string `json:"outcome"`
|
|
95
|
+
OptionID string `json:"optionId,omitempty"`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// PermissionResponse is `session/request_permission` result.
|
|
99
|
+
type PermissionResponse struct {
|
|
100
|
+
Outcome PermissionOutcome `json:"outcome"`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Conn is what the adapter needs from the ACP connection to the client
|
|
104
|
+
// (the SDK's AgentSideConnection subset pi-acp uses).
|
|
105
|
+
type Conn interface {
|
|
106
|
+
// SessionUpdate sends a `session/update` notification.
|
|
107
|
+
SessionUpdate(sessionID string, update Update) error
|
|
108
|
+
// RequestPermission sends `session/request_permission` and waits for the answer.
|
|
109
|
+
RequestPermission(req PermissionRequest) (PermissionResponse, error)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Proc is the pi RPC child process as the session sees it (pirpc.Process implements it).
|
|
113
|
+
type Proc interface {
|
|
114
|
+
OnEvent(handler func(Event)) (unsubscribe func())
|
|
115
|
+
Prompt(message string, images []Image) error
|
|
116
|
+
Abort() error
|
|
117
|
+
GetState() (map[string]any, error)
|
|
118
|
+
GetAvailableModels() (map[string]any, error)
|
|
119
|
+
SetModel(provider, modelID string) error
|
|
120
|
+
GetAvailableThinkingLevels() ([]string, error)
|
|
121
|
+
SetThinkingLevel(level string) error
|
|
122
|
+
SetFollowUpMode(mode string) error
|
|
123
|
+
SetSteeringMode(mode string) error
|
|
124
|
+
// Compact runs manual compaction; customInstructions "" sends none.
|
|
125
|
+
Compact(customInstructions string) (map[string]any, error)
|
|
126
|
+
SetAutoCompaction(enabled bool) error
|
|
127
|
+
// GetSessionStats asks for get_session_stats; timeout 0 waits without a limit.
|
|
128
|
+
GetSessionStats(timeoutMs int) (SessionStats, error)
|
|
129
|
+
SetSessionName(name string) error
|
|
130
|
+
// ExportHTML returns the path pig wrote; outputPath "" lets pig choose.
|
|
131
|
+
ExportHTML(outputPath string) (string, error)
|
|
132
|
+
GetMessages() (map[string]any, error)
|
|
133
|
+
GetCommands() (map[string]any, error)
|
|
134
|
+
SendExtensionUIResponse(resp map[string]any) error
|
|
135
|
+
Dispose()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Image is a pi image attachment: {type:"image", mimeType, data}.
|
|
139
|
+
type Image = map[string]any
|
|
140
|
+
|
|
141
|
+
// SessionStats is `get_session_stats` data, kept as a JSON object.
|
|
142
|
+
type SessionStats = map[string]any
|
|
143
|
+
|
|
144
|
+
// SessionStatsTimeoutMs bounds the auxiliary context-usage request.
|
|
145
|
+
const SessionStatsTimeoutMs = 1000
|
|
146
|
+
|
|
147
|
+
// SpawnParams are the options for starting a pi child.
|
|
148
|
+
type SpawnParams struct {
|
|
149
|
+
Cwd string
|
|
150
|
+
// PiCommand overrides the executable ("" = default).
|
|
151
|
+
PiCommand string
|
|
152
|
+
// SessionPath makes pi persist to this exact session file (--session).
|
|
153
|
+
SessionPath string
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// SpawnFunc starts a pi RPC child. The agent uses pirpc.Spawn unless a test injects another.
|
|
157
|
+
type SpawnFunc func(SpawnParams) (Proc, error)
|
|
158
|
+
|
|
159
|
+
// TurnResult is the outcome of one prompt turn.
|
|
160
|
+
type TurnResult struct {
|
|
161
|
+
Reason StopReason
|
|
162
|
+
Err error
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ActiveSession is a live session as the agent uses it (Session implements it).
|
|
166
|
+
type ActiveSession interface {
|
|
167
|
+
ID() string
|
|
168
|
+
Cwd() string
|
|
169
|
+
Proc() Proc
|
|
170
|
+
// Prompt starts the turn now, or queues it behind the running one.
|
|
171
|
+
Prompt(message string, images []Image) <-chan TurnResult
|
|
172
|
+
Cancel() error
|
|
173
|
+
WasCancelRequested() bool
|
|
174
|
+
PublishContextUsage()
|
|
175
|
+
SetStartupInfo(text string)
|
|
176
|
+
SendStartupInfoIfPending()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// SessionCreateParams describe a session to create or restore.
|
|
180
|
+
type SessionCreateParams struct {
|
|
181
|
+
Cwd string
|
|
182
|
+
McpServers []any
|
|
183
|
+
Conn Conn
|
|
184
|
+
FileCommands []FileSlashCommand
|
|
185
|
+
PiCommand string
|
|
186
|
+
// Proc is set when restoring around an already started child.
|
|
187
|
+
Proc Proc
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// SessionRegistry is the SessionManager of the original.
|
|
191
|
+
type SessionRegistry interface {
|
|
192
|
+
Create(p SessionCreateParams) (ActiveSession, error)
|
|
193
|
+
MaybeGet(sessionID string) ActiveSession
|
|
194
|
+
Get(sessionID string) (ActiveSession, error)
|
|
195
|
+
GetOrCreate(sessionID string, p SessionCreateParams) ActiveSession
|
|
196
|
+
Close(sessionID string)
|
|
197
|
+
CloseAllExcept(keepSessionID string)
|
|
198
|
+
DisposeAll()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// RPCError implements jsonrpc.Coded.
|
|
202
|
+
func (e *RequestError) RPCError() (int, string, any) { return e.Code, e.Message, e.Data }
|