@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
package/CREDITS.md ADDED
@@ -0,0 +1,23 @@
1
+ # Credits
2
+
3
+ `extensions/acp/cmd/pig-acp` is a Go port of **pi-acp**, https://github.com/svkozak/pi-acp, by
4
+ **Sergii Kozak** (MIT, Copyright (c) 2025 Sergii Kozak): the Agent Client Protocol adapter for the Pi
5
+ coding agent, which the port adapts to PiG.
6
+
7
+ - Original: `src/` and `test/` of pi-acp 0.0.34, commit `b0581c9c1d675e634234674484247008b03d69b4`.
8
+ - The unmodified original, with its README, `package.json` and license, is kept at
9
+ [`port/oracle/pi-acp`](port/oracle/pi-acp) as the oracle the Go tests were ported from. Its license is at
10
+ [`port/oracle/pi-acp/LICENSE`](port/oracle/pi-acp/LICENSE).
11
+ - What was modified: nothing of the original is modified in place. The port is a separate Go implementation with the
12
+ same structure (`agent`, `session`, `translate`, `pi-rpc`), PiG's directories and executable in place of Pi's,
13
+ the adapter's own JSON-RPC transport in place of the ACP SDK, and the additions listed in
14
+ [`port/PORT.md`](port/PORT.md). The ported tests keep the original test names.
15
+
16
+ The protocol is the **Agent Client Protocol**, https://agentclientprotocol.com. The JSON schema in
17
+ [`port/schema/schema.json`](port/schema/schema.json) is `schema/schema.json` of
18
+ `@agentclientprotocol/sdk` 0.26.0 by **Zed Industries** (Apache-2.0, https://github.com/agentclientprotocol/typescript-sdk,
19
+ tag `v0.26.0`, commit `73bc30649b650de320340c782733bf69a545bd28`), unmodified, with its license at
20
+ [`port/schema/LICENSE`](port/schema/LICENSE). It is used only to check the adapter's messages in tests.
21
+
22
+ The Go code, the scenarios and the tests were written for this Package by Michael Kinsy and are MIT
23
+ licensed like the rest of Pigpen.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Sergii Kozak
4
+ Copyright (c) 2026 Michael Kinsy (the Go port)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # acp: Agent Client Protocol for PiG
2
+
3
+ Lets an editor that speaks the [Agent Client Protocol](https://agentclientprotocol.com) (ACP), such as
4
+ [Zed](https://zed.dev), drive a PiG agent: sessions, streamed answers, tool calls with diffs and terminal output,
5
+ cancel, model and thinking-level selection, context usage, and permission requests for extension dialogs.
6
+
7
+ It is a Go port of [pi-acp](https://github.com/svkozak/pi-acp) by Sergii Kozak (MIT). See [CREDITS.md](CREDITS.md)
8
+ and the port record, [port/PORT.md](port/PORT.md). **Pinned protocol: ACP version 1, schema of
9
+ `@agentclientprotocol/sdk` 0.26.0.** Built against PiG 0.4.0 (Pi 1.0.0); the numbers under Evidence were first recorded on PiG 0.3.0, and the end-to-end scenarios, the ACP reference client run and the Go tests pass again on 0.4.0.
10
+
11
+ The Package has two parts:
12
+
13
+ | Part | What it is |
14
+ |---|---|
15
+ | [`extensions/acp/cmd/pig-acp`](extensions/acp/cmd/pig-acp) | **The protocol entrypoint**, a separate executable with no dependency (not even the PiG SDK). The editor starts it and speaks ACP on its stdin and stdout; it starts `pig --mode rpc` for each session. It is not the TUI and shares nothing with it. |
16
+ | [`extensions/acp`](extensions/acp) | A small Go extension on the public SDK. `/acp` prints the editor configuration for the running agent and the limits below. |
17
+
18
+ An extension cannot speak ACP itself: a fused extension must not write protocol bytes to the host's stdout. So
19
+ **installing this Package, or building the `acp` Piglet, does not make the Piglet Binary an ACP server.** The editor
20
+ runs `pig-acp`, and `pig-acp --pig <your pig or Piglet Binary>` runs the agent. There is no one-command Binary yet
21
+ (see [RELEASE-BLOCKERS.md](../../RELEASE-BLOCKERS.md)).
22
+
23
+ ## Use
24
+
25
+ Build the companion (Go 1.26 or later) and put it on your `PATH`:
26
+
27
+ ```sh
28
+ cd components/acp/extensions/acp/cmd/pig-acp
29
+ go build -trimpath -o pig-acp .
30
+ pig-acp --version # pig-acp 0.1.0 (ACP protocol version 1)
31
+ ```
32
+
33
+ Zed, `settings.json` (`/acp` prints this with your paths filled in):
34
+
35
+ ```json
36
+ {
37
+ "agent_servers": {
38
+ "PiG": { "type": "custom", "command": "pig-acp", "args": ["--pig", "/path/to/pig"], "env": {} }
39
+ }
40
+ }
41
+ ```
42
+
43
+ Other ACP clients run `pig-acp --pig <pig>` and speak newline-delimited JSON-RPC on stdio. `--pig` also takes a built Piglet
44
+ Binary (`--pig ./acp`). `--piglet <name-or-file>` starts pig with `--piglet ...`, and `--pig-arg <arg>` (repeatable) passes any
45
+ other argument. Without `--pig`, `PIG_ACP_PIG_COMMAND` (or `PI_ACP_PI_COMMAND`) is used, then `pig`.
46
+
47
+ Signing in: if pig has no model or key, `session/new` fails with the ACP "authentication required" error (-32000) and one
48
+ terminal auth method, `pig-acp --terminal-login`, which starts pig interactively in the editor's terminal.
49
+
50
+ | Flag or variable | Meaning |
51
+ |---|---|
52
+ | `--pig <command>` | the pig executable or Piglet Binary to drive |
53
+ | `--piglet <value>`, `--pig-arg <arg>` | extra pig arguments |
54
+ | `--terminal-login` | start pig interactively (sign in, configure keys) and exit with its status |
55
+ | `--version`, `--help` | print and exit |
56
+ | `PIG_ACP_PIG_COMMAND` | the pig executable when `--pig` is not given |
57
+ | `PIG_ACP_ENABLE_EMBEDDED_CONTEXT=true` | advertise embedded-context prompts (off by default) |
58
+ | `PIG_HOME`, `PIG_CODING_AGENT_DIR`, `PIG_CODING_AGENT_SESSION_DIR`, `PIG_USE_PI_DIRS=1` | pig's own directories are used as pig uses them (a leading `~` is expanded) |
59
+
60
+ The session map (ACP session id to pig session file) is `<PIG_HOME>/pig-acp/session-map.json`. Sessions are pig's own session
61
+ files, so they show up in `pig` too. `pig-acp` reads prompt templates from `<agent dir>/prompts` and `<cwd>/.pig/prompts` and
62
+ settings from `<agent dir>/settings.json` and `<cwd>/.pig/settings.json`. It never reads `auth.json` for itself; like pi-acp, it
63
+ reads a file that pig's `edit` or `write` tool changes, before and after, to send the diff to the editor.
64
+
65
+ ## What is implemented
66
+
67
+ Advertised in `initialize`, and only these: `loadSession`, prompt `image` (embedded context only by the variable above; no
68
+ audio), `sessionCapabilities` `list` and `delete`, MCP `http` and `sse` **false**.
69
+
70
+ - `session/new`, `session/load` (history replayed, including tool results), `session/list` (paged by 50, scoped to the last
71
+ cwd), `session/delete` (idempotent), `session/prompt`, `session/cancel`, `session/set_mode` (thinking level),
72
+ `session/set_config_option` (model, thinking level), `session/set_model`, `authenticate`.
73
+ - Streaming: `agent_message_chunk`, `agent_thought_chunk`, `tool_call` and `tool_call_update` with locations, structured
74
+ diffs for edit and write, `execute` calls with terminal output for bash, `usage_update`, `available_commands_update`,
75
+ `config_option_update`, `current_mode_update`, `session_info_update`.
76
+ - Adapter-side slash commands that never reach the model: `/compact`, `/autocompact`, `/session`, `/name`, `/steering`,
77
+ `/follow-up`, `/changelog`, `/export`. Prompt template files expand as `/name args`.
78
+ - **Permissions.** An extension `select` or `confirm` dialog becomes `session/request_permission`; the editor's answer is
79
+ sent back to the extension. A client that fails or cancels means "cancelled". Nothing is ever executed locally in place of an
80
+ editor's answer. `input` and `editor` dialogs are cancelled with a visible notice; `notify` becomes a chunk with its level.
81
+ - One live pig child per ACP connection: a new session or load closes the previous child. Closing stdin (or SIGINT/SIGTERM)
82
+ stops every child and exits 0. Nothing but protocol messages is written to stdout.
83
+
84
+ ## Not supported (by design, as upstream)
85
+
86
+ - File system and terminal delegation: `pig-acp` never calls `fs/*` or `terminal/*` on the editor; pig reads and writes files itself.
87
+ - MCP servers passed by the editor are accepted and stored, never started (PiG's own MCP support is configured in PiG).
88
+ - Extension slash commands (`/name` of an extension is not advertised, and as a prompt it runs without ending a turn: drive
89
+ extensions through tools). `session/fork`, `session/resume`, `session/close` are Method not found.
90
+ - The npm "New version available" notice: PiG has no npm package (`pig update` uses a signed manifest).
91
+
92
+ ## Evidence
93
+
94
+ Everything below was first run on PiG 0.3.0 and re-run on 0.4.0 (the 11 end-to-end scenarios, the reference client's 30 checked messages, the Go tests) with a temporary `HOME`, `PIG_HOME` and agent directories.
95
+
96
+ - **Twins of the upstream tests:** 124 exact twins with their upstream titles plus 1 named skip, checked by
97
+ `pigeq twins check` against the upstream ledger (`port/upstream-tests.json`). Two more skipped tests name the remaining gaps (the npm update notice, the compaction event names); see `port/PORT.md`.
98
+ - **Unit and protocol tests:** `go test -race ./...` in both modules (a scripted ACP client over pipes against `Serve`).
99
+ - **End to end:** 11 scenarios (streaming, bash, diff, cancel, load/list/delete across two adapter runs, config options, a real
100
+ permission round trip through a Go extension tool, no-model auth error, missing pig, MCP accepted, exit on stdin close) pass against
101
+ a real `pig` **and against the built `acp` Piglet Binary** (`PIG_ACP_E2E_PIG=<binary> go test -run E2E`).
102
+ - **Reference client:** `components/acp/port/interop` runs the ACP SDK's `ClientSideConnection` 0.26.0 against `pig-acp` and the
103
+ Piglet Binary and validates every message against the pinned schema: `npm ci --ignore-scripts` in `port/interop`, then
104
+ `PIG_ACP=<pig-acp> PIG=<pig or Binary> PIGEQ=<pigeq> npm run test:acp-interop`. The run needs the `pigeq` built from
105
+ this repository (`PIG_BIN=<pig> npm run build:pigeq` writes `dist/bin/pigeq`), which has the `llm` command; so does the twin
106
+ ledger check (`pigeq twins check`).
107
+ - **Mutations:** 105 deliberate defects (`port/mutations.json`, `node components/acp/port/mutate.mjs`), 104 killed and 1 documented
108
+ equivalent; results in `port/mutation-results.txt`.
109
+
110
+ **Not verified:** Zed and JetBrains themselves (no editor could be run here). The reference client is the interop evidence.
@@ -0,0 +1,168 @@
1
+ package main
2
+
3
+ // A scripted OpenAI-compatible chat-completions server, so the end-to-end tests drive a real
4
+ // `pig --mode rpc` without credentials or a network (the same idea as the equivalence harness).
5
+
6
+ import (
7
+ "encoding/json"
8
+ "fmt"
9
+ "net"
10
+ "net/http"
11
+ "strings"
12
+ "sync"
13
+ "testing"
14
+ "time"
15
+ )
16
+
17
+ type llmCall struct {
18
+ Name string
19
+ Args map[string]any
20
+ }
21
+
22
+ // llmTurn is one scripted model reply.
23
+ type llmTurn struct {
24
+ Text string
25
+ Calls []llmCall
26
+ // Chunks streams Text in this many pieces (default 1).
27
+ Chunks int
28
+ // Hold blocks the reply after its first chunk until the channel closes or the client disconnects.
29
+ Hold chan struct{}
30
+ // Started is closed when the first chunk of this turn has been sent.
31
+ Started chan struct{}
32
+ }
33
+
34
+ type fakeLLM struct {
35
+ t *testing.T
36
+ ln net.Listener
37
+ srv *http.Server
38
+ mu sync.Mutex
39
+ turns []llmTurn
40
+ next int
41
+ // requests records the last user text of each request.
42
+ requests []string
43
+ }
44
+
45
+ func startFakeLLM(t *testing.T, turns ...llmTurn) *fakeLLM {
46
+ t.Helper()
47
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
48
+ if err != nil {
49
+ t.Fatal(err)
50
+ }
51
+ f := &fakeLLM{t: t, ln: ln, turns: turns}
52
+ mux := http.NewServeMux()
53
+ mux.HandleFunc("/v1/chat/completions", f.handle)
54
+ f.srv = &http.Server{Handler: mux}
55
+ go func() { _ = f.srv.Serve(ln) }()
56
+ t.Cleanup(func() { _ = f.srv.Close() })
57
+ return f
58
+ }
59
+
60
+ func (f *fakeLLM) baseURL() string { return "http://" + f.ln.Addr().String() + "/v1" }
61
+
62
+ func (f *fakeLLM) seen() []string {
63
+ f.mu.Lock()
64
+ defer f.mu.Unlock()
65
+ return append([]string(nil), f.requests...)
66
+ }
67
+
68
+ func (f *fakeLLM) handle(w http.ResponseWriter, r *http.Request) {
69
+ var req struct {
70
+ Messages []struct {
71
+ Role string `json:"role"`
72
+ Content json.RawMessage `json:"content"`
73
+ } `json:"messages"`
74
+ }
75
+ _ = json.NewDecoder(r.Body).Decode(&req)
76
+ last := ""
77
+ for _, m := range req.Messages {
78
+ if m.Role == "user" {
79
+ var s string
80
+ if json.Unmarshal(m.Content, &s) == nil {
81
+ last = s
82
+ } else {
83
+ var parts []struct {
84
+ Text string `json:"text"`
85
+ }
86
+ _ = json.Unmarshal(m.Content, &parts)
87
+ if len(parts) > 0 {
88
+ last = parts[0].Text
89
+ }
90
+ }
91
+ }
92
+ }
93
+ f.mu.Lock()
94
+ f.requests = append(f.requests, last)
95
+ i := f.next
96
+ f.next++
97
+ f.mu.Unlock()
98
+ if i >= len(f.turns) {
99
+ http.Error(w, "llm script exhausted", http.StatusInternalServerError)
100
+ return
101
+ }
102
+ turn := f.turns[i]
103
+ w.Header().Set("Content-Type", "text/event-stream")
104
+ flusher, _ := w.(http.Flusher)
105
+ send := func(delta map[string]any, finish any) {
106
+ b, _ := json.Marshal(map[string]any{"id": fmt.Sprintf("acp-%d", i), "object": "chat.completion.chunk", "created": 1, "model": "acp-1",
107
+ "choices": []any{map[string]any{"index": 0, "delta": delta, "finish_reason": finish}}})
108
+ fmt.Fprintf(w, "data: %s\n\n", b)
109
+ flusher.Flush()
110
+ }
111
+ send(map[string]any{"role": "assistant", "content": ""}, nil)
112
+ chunks := turn.Chunks
113
+ if chunks < 1 {
114
+ chunks = 1
115
+ }
116
+ if turn.Text != "" {
117
+ pieces := splitText(turn.Text, chunks)
118
+ for n, p := range pieces {
119
+ send(map[string]any{"content": p}, nil)
120
+ if n == 0 && turn.Started != nil {
121
+ close(turn.Started)
122
+ }
123
+ if n == 0 && turn.Hold != nil {
124
+ select {
125
+ case <-turn.Hold:
126
+ case <-r.Context().Done():
127
+ return
128
+ }
129
+ }
130
+ }
131
+ } else if turn.Started != nil {
132
+ close(turn.Started)
133
+ }
134
+ finish := "stop"
135
+ for j, c := range turn.Calls {
136
+ args, _ := json.Marshal(c.Args)
137
+ send(map[string]any{"tool_calls": []any{map[string]any{"index": j, "id": fmt.Sprintf("call_%d_%d", i, j), "type": "function",
138
+ "function": map[string]any{"name": c.Name, "arguments": string(args)}}}}, nil)
139
+ finish = "tool_calls"
140
+ }
141
+ send(map[string]any{}, finish)
142
+ usage, _ := json.Marshal(map[string]any{"id": fmt.Sprintf("acp-%d", i), "object": "chat.completion.chunk", "created": 1, "model": "acp-1", "choices": []any{},
143
+ "usage": map[string]any{"prompt_tokens": 1234, "completion_tokens": 5, "total_tokens": 1239}})
144
+ fmt.Fprintf(w, "data: %s\n\ndata: [DONE]\n\n", usage)
145
+ flusher.Flush()
146
+ }
147
+
148
+ func splitText(s string, n int) []string {
149
+ if n <= 1 || len(s) < n {
150
+ return []string{s}
151
+ }
152
+ words := strings.SplitAfter(s, " ")
153
+ if len(words) < n {
154
+ return []string{s}
155
+ }
156
+ per := (len(words) + n - 1) / n
157
+ var out []string
158
+ for i := 0; i < len(words); i += per {
159
+ end := i + per
160
+ if end > len(words) {
161
+ end = len(words)
162
+ }
163
+ out = append(out, strings.Join(words[i:end], ""))
164
+ }
165
+ return out
166
+ }
167
+
168
+ var _ = time.Second