@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,289 @@
1
+ package acp
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ "path/filepath"
7
+ "regexp"
8
+ "strings"
9
+ )
10
+
11
+ var unsafeIDChars = regexp.MustCompile(`[^a-zA-Z0-9_-]`)
12
+
13
+ func (a *Agent) say(sessionID, text string) error {
14
+ return a.conn.SessionUpdate(sessionID, textChunk("agent_message_chunk", text))
15
+ }
16
+
17
+ func endTurn() (PromptResponse, error) { return PromptResponse{StopReason: StopEndTurn}, nil }
18
+
19
+ // Prompt runs one prompt turn, or an adapter-side slash command (compact, session, name, steering,
20
+ // follow-up, changelog, export, autocompact). Those never reach the model.
21
+ func (a *Agent) Prompt(req PromptRequest) (PromptResponse, error) {
22
+ sess, err := a.restoreSession(req.SessionID, "", nil)
23
+ if err != nil {
24
+ return PromptResponse{}, err
25
+ }
26
+ message, images := PromptToPiMessage(req.Prompt)
27
+
28
+ if len(images) == 0 && strings.HasPrefix(strings.TrimLeftFunc(message, isJSSpace), "/") {
29
+ trimmed := jsTrim(message)
30
+ space := strings.Index(trimmed, " ")
31
+ cmd, argStr := trimmed[1:], ""
32
+ if space != -1 {
33
+ cmd, argStr = trimmed[1:space], trimmed[space+1:]
34
+ }
35
+ args := ParseCommandArgs(argStr)
36
+ if handled, resp, err := a.builtin(sess, cmd, args); handled {
37
+ return resp, err
38
+ }
39
+ }
40
+
41
+ result := <-sess.Prompt(message, images)
42
+ if result.Err != nil {
43
+ return PromptResponse{}, result.Err
44
+ }
45
+ reason := result.Reason
46
+ // ACP has no "error" stop reason: a failed turn ends as end_turn unless it was cancelled.
47
+ if reason == StopError {
48
+ reason = StopEndTurn
49
+ if sess.WasCancelRequested() {
50
+ reason = StopCancelled
51
+ }
52
+ }
53
+ return PromptResponse{StopReason: reason}, nil
54
+ }
55
+
56
+ func isJSSpace(r rune) bool { return jsTrim(string(r)) == "" }
57
+
58
+ func (a *Agent) builtin(sess ActiveSession, cmd string, args []string) (bool, PromptResponse, error) {
59
+ id, proc := sess.ID(), sess.Proc()
60
+ done := func(err error) (bool, PromptResponse, error) {
61
+ if err != nil {
62
+ return true, PromptResponse{}, err
63
+ }
64
+ r, _ := endTurn()
65
+ return true, r, nil
66
+ }
67
+ switch cmd {
68
+ case "compact":
69
+ custom := jsTrim(strings.Join(args, " "))
70
+ res, err := proc.Compact(custom)
71
+ if err != nil {
72
+ return done(err)
73
+ }
74
+ header := "Compaction completed."
75
+ if custom != "" {
76
+ header += " (custom instructions applied)"
77
+ }
78
+ if n, ok := asNumber(res["tokensBefore"]); ok {
79
+ header += "\nTokens before: " + jsNumber(n)
80
+ }
81
+ text := header
82
+ if summary, ok := res["summary"].(string); ok && summary != "" {
83
+ text += "\n\n" + summary
84
+ }
85
+ return done(a.say(id, text))
86
+
87
+ case "session":
88
+ stats, err := proc.GetSessionStats(0)
89
+ if err != nil {
90
+ return done(err)
91
+ }
92
+ return done(a.say(id, sessionStatsText(stats)))
93
+
94
+ case "name":
95
+ name := jsTrim(strings.Join(args, " "))
96
+ if name == "" {
97
+ return done(a.say(id, "Usage: /name <name>"))
98
+ }
99
+ if err := proc.SetSessionName(name); err != nil {
100
+ msg := err.Error()
101
+ hint := ""
102
+ if strings.Contains(strings.ToLower(msg), "set_session_name") {
103
+ hint = " This requires a newer pi version that supports `set_session_name` in RPC mode."
104
+ }
105
+ return done(a.say(id, "Failed to set session name: "+msg+hint))
106
+ }
107
+ if err := a.send(id, Update{"sessionUpdate": "session_info_update", "title": name, "updatedAt": nowISO()}); err != nil {
108
+ return done(err)
109
+ }
110
+ return done(a.say(id, "Session name set: "+name))
111
+
112
+ case "steering":
113
+ return a.deliveryMode(sess, "Steering", "steering", "steeringMode", args, proc.SetSteeringMode)
114
+ case "follow-up":
115
+ return a.deliveryMode(sess, "Follow-up", "follow-up", "followUpMode", args, proc.SetFollowUpMode)
116
+
117
+ case "changelog":
118
+ return done(a.changelog(id))
119
+
120
+ case "export":
121
+ return done(a.export(sess))
122
+
123
+ case "autocompact":
124
+ mode := "toggle"
125
+ if len(args) > 0 {
126
+ mode = strings.ToLower(args[0])
127
+ }
128
+ var enabled bool
129
+ switch mode {
130
+ case "on", "true", "enable", "enabled":
131
+ enabled = true
132
+ case "off", "false", "disable", "disabled":
133
+ enabled = false
134
+ default:
135
+ state, err := proc.GetState()
136
+ if err != nil {
137
+ return done(err)
138
+ }
139
+ enabled = !truthy(state["autoCompactionEnabled"])
140
+ }
141
+ if err := proc.SetAutoCompaction(enabled); err != nil {
142
+ return done(err)
143
+ }
144
+ word := "disabled"
145
+ if enabled {
146
+ word = "enabled"
147
+ }
148
+ return done(a.say(id, "Auto-compaction "+word+"."))
149
+ }
150
+ return false, PromptResponse{}, nil
151
+ }
152
+
153
+ func nowISO() string { return timeNowUTC().Format("2006-01-02T15:04:05.000Z") }
154
+
155
+ func sessionStatsText(stats SessionStats) string {
156
+ var lines []string
157
+ if v := stats["sessionId"]; truthy(v) {
158
+ lines = append(lines, "Session: "+jsString(v))
159
+ }
160
+ if v := stats["sessionFile"]; truthy(v) {
161
+ lines = append(lines, "Session file: "+jsString(v))
162
+ }
163
+ if n, ok := asNumber(stats["totalMessages"]); ok {
164
+ lines = append(lines, "Messages: "+jsNumber(n))
165
+ }
166
+ if n, ok := asNumber(stats["cost"]); ok {
167
+ lines = append(lines, "Cost: "+jsNumber(n))
168
+ }
169
+ if t := asObject(stats["tokens"]); t != nil {
170
+ var parts []string
171
+ for _, p := range [][2]string{{"input", "in"}, {"output", "out"}, {"cacheRead", "cache read"}, {"cacheWrite", "cache write"}, {"total", "total"}} {
172
+ if n, ok := asNumber(t[p[0]]); ok {
173
+ parts = append(parts, p[1]+" "+jsNumber(n))
174
+ }
175
+ }
176
+ if len(parts) > 0 {
177
+ lines = append(lines, "Tokens: "+strings.Join(parts, ", "))
178
+ }
179
+ }
180
+ if len(lines) > 0 {
181
+ return strings.Join(lines, "\n")
182
+ }
183
+ return "Session stats:\n" + jsonStringify(stats)
184
+ }
185
+
186
+ func (a *Agent) deliveryMode(sess ActiveSession, label, cmd, stateKey string, args []string, set func(string) error) (bool, PromptResponse, error) {
187
+ id, proc := sess.ID(), sess.Proc()
188
+ fail := func(err error) (bool, PromptResponse, error) { return true, PromptResponse{}, err }
189
+ ok := func(err error) (bool, PromptResponse, error) {
190
+ if err != nil {
191
+ return fail(err)
192
+ }
193
+ r, _ := endTurn()
194
+ return true, r, nil
195
+ }
196
+ mode := ""
197
+ if len(args) > 0 {
198
+ mode = strings.ToLower(args[0])
199
+ }
200
+ state, err := proc.GetState()
201
+ if err != nil {
202
+ return fail(err)
203
+ }
204
+ current := ""
205
+ if v, has := state[stateKey]; has && v != nil {
206
+ current = jsString(v)
207
+ }
208
+ if mode == "" {
209
+ if current == "" {
210
+ current = "unknown"
211
+ }
212
+ return ok(a.say(id, label+" mode: "+current))
213
+ }
214
+ if mode != "all" && mode != "one-at-a-time" {
215
+ return ok(a.say(id, "Usage: /"+cmd+" all | /"+cmd+" one-at-a-time"))
216
+ }
217
+ if err := set(mode); err != nil {
218
+ return fail(err)
219
+ }
220
+ return ok(a.say(id, label+" mode set to: "+mode))
221
+ }
222
+
223
+ func (a *Agent) changelog(id string) error {
224
+ var path string
225
+ if a.changelogPath != nil {
226
+ path = a.changelogPath()
227
+ } else {
228
+ path = FindChangelog(a.pigCommand())
229
+ }
230
+ if path == "" {
231
+ return a.say(id, "Changelog not found (couldn't locate pig installation).")
232
+ }
233
+ raw, err := os.ReadFile(path)
234
+ if err != nil {
235
+ return a.say(id, "Failed to read changelog: "+err.Error())
236
+ }
237
+ text := string(raw)
238
+ // Keep it a reasonable size in chat.
239
+ if r := []rune(text); len(r) > maxChangelogChars {
240
+ text = string(r[:maxChangelogChars]) + "\n\n...(truncated)..."
241
+ }
242
+ return a.say(id, text)
243
+ }
244
+
245
+ func (a *Agent) export(sess ActiveSession) error {
246
+ id, proc := sess.ID(), sess.Proc()
247
+ // Always exported into the session cwd; no user-provided path. pig's export_html reads the session
248
+ // file, and fails on a missing or empty one, so that is guarded here.
249
+ state, err := proc.GetState()
250
+ if err != nil {
251
+ return err
252
+ }
253
+ file, _ := state["sessionFile"].(string)
254
+ count := 0.0
255
+ if n, ok := asNumber(state["messageCount"]); ok {
256
+ count = n
257
+ }
258
+ exists := false
259
+ if file != "" {
260
+ _, serr := os.Stat(file)
261
+ exists = serr == nil
262
+ }
263
+ if file == "" || count == 0 || !exists {
264
+ return a.say(id, "Nothing to export yet (no session messages). Send a prompt first.")
265
+ }
266
+ raw, err := os.ReadFile(file)
267
+ if err != nil {
268
+ return a.say(id, "Couldn't read session file for export. Try sending a prompt first.")
269
+ }
270
+ if jsTrim(string(raw)) == "" {
271
+ return a.say(id, "Nothing to export yet (empty session file). Send a prompt first.")
272
+ }
273
+ safe := unsafeIDChars.ReplaceAllString(id, "_")
274
+ out := filepath.Join(sess.Cwd(), "pi-session-"+safe+".html")
275
+ path, err := proc.ExportHTML(out)
276
+ if err != nil {
277
+ return a.say(id, "Export failed: "+err.Error())
278
+ }
279
+ if path == "" {
280
+ return a.say(id, "Export failed: no output path returned by pi.")
281
+ }
282
+ uri := "file://" + path
283
+ // A short prefix plus a resource link: many clients concatenate chunks into one message.
284
+ if err := a.say(id, "Session exported: "); err != nil {
285
+ return err
286
+ }
287
+ return a.conn.SessionUpdate(id, Update{"sessionUpdate": "agent_message_chunk", "content": map[string]any{
288
+ "type": "resource_link", "name": fmt.Sprintf("pi-session-%s.html", safe), "uri": uri, "mimeType": "text/html", "title": "Session exported"}})
289
+ }