@pi-in-go/pigpen-ahp 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 +27 -0
- package/LICENSE +23 -0
- package/README.md +148 -0
- package/extensions/ahp/access_options_test.go +116 -0
- package/extensions/ahp/extension.go +54 -0
- package/extensions/ahp/go.mod +12 -0
- package/extensions/ahp/go.sum +2 -0
- package/extensions/ahp/go.work +8 -0
- package/extensions/ahp/internal/channels/chat.go +110 -0
- package/extensions/ahp/internal/channels/root.go +32 -0
- package/extensions/ahp/internal/channels/session.go +128 -0
- package/extensions/ahp/internal/channels/session_test.go +108 -0
- package/extensions/ahp/internal/compose/compose.go +153 -0
- package/extensions/ahp/internal/compose/compose_test.go +120 -0
- package/extensions/ahp/internal/gaps/gaps_test.go +88 -0
- package/extensions/ahp/internal/host/handshake_test.go +206 -0
- package/extensions/ahp/internal/host/host.go +1305 -0
- package/extensions/ahp/internal/host/reconnect_test.go +197 -0
- package/extensions/ahp/internal/host/replaywindow_test.go +31 -0
- package/extensions/ahp/internal/host/schema_test.go +79 -0
- package/extensions/ahp/internal/host/store.go +291 -0
- package/extensions/ahp/internal/host/store_update_test.go +85 -0
- package/extensions/ahp/internal/host/subscriptions_test.go +160 -0
- package/extensions/ahp/internal/host/surface_test.go +95 -0
- package/extensions/ahp/internal/host/workarounds.go +458 -0
- package/extensions/ahp/internal/host/workarounds_test.go +96 -0
- package/extensions/ahp/internal/live/live.go +328 -0
- package/extensions/ahp/internal/live/live_test.go +91 -0
- package/extensions/ahp/internal/live/store.go +113 -0
- package/extensions/ahp/internal/mapper/activity.go +199 -0
- package/extensions/ahp/internal/mapper/activity_test.go +188 -0
- package/extensions/ahp/internal/mapper/bench_test.go +38 -0
- package/extensions/ahp/internal/mapper/event_mapper_test.go +380 -0
- package/extensions/ahp/internal/mapper/fixtures_test.go +387 -0
- package/extensions/ahp/internal/mapper/helpers_test.go +251 -0
- package/extensions/ahp/internal/mapper/js.go +131 -0
- package/extensions/ahp/internal/mapper/mapper.go +600 -0
- package/extensions/ahp/internal/mapper/message_input_test.go +196 -0
- package/extensions/ahp/internal/mapper/messageinput.go +272 -0
- package/extensions/ahp/internal/mapper/schema_helpers_test.go +5 -0
- package/extensions/ahp/internal/mapper/testdata/LICENSE-pi-ahp +21 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/abort.json +711 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/bash-long-output.json +1312 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/compaction.json +6285 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/parallel-tools.json +2622 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/plain-text.json +1856 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/single-tool.json +1128 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/steering.json +3926 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-bash.json +2372 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-edit.json +3983 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-error.json +2850 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-find.json +1162 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-grep.json +2783 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-loop.json +1693 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-ls.json +2427 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-write.json +1271 -0
- package/extensions/ahp/internal/mapper/title.go +34 -0
- package/extensions/ahp/internal/mapper/usermsg.go +87 -0
- package/extensions/ahp/internal/pi/activeturn_test.go +296 -0
- package/extensions/ahp/internal/pi/backend.go +64 -0
- package/extensions/ahp/internal/pi/catalogue.go +370 -0
- package/extensions/ahp/internal/pi/catalogue_test.go +271 -0
- package/extensions/ahp/internal/pi/chatdriver.go +533 -0
- package/extensions/ahp/internal/pi/chatdriver_test.go +416 -0
- package/extensions/ahp/internal/pi/clientactions.go +212 -0
- package/extensions/ahp/internal/pi/clientactions_test.go +273 -0
- package/extensions/ahp/internal/pi/completions.go +230 -0
- package/extensions/ahp/internal/pi/completions_test.go +272 -0
- package/extensions/ahp/internal/pi/config_test.go +269 -0
- package/extensions/ahp/internal/pi/deletesession.go +69 -0
- package/extensions/ahp/internal/pi/deletesession_test.go +153 -0
- package/extensions/ahp/internal/pi/disposal_test.go +362 -0
- package/extensions/ahp/internal/pi/fixture_test.go +308 -0
- package/extensions/ahp/internal/pi/foreign_test.go +122 -0
- package/extensions/ahp/internal/pi/harness_test.go +83 -0
- package/extensions/ahp/internal/pi/history.go +269 -0
- package/extensions/ahp/internal/pi/hydrated_test.go +171 -0
- package/extensions/ahp/internal/pi/hydratedlifecycle_test.go +196 -0
- package/extensions/ahp/internal/pi/hydration_test.go +209 -0
- package/extensions/ahp/internal/pi/hydrator.go +212 -0
- package/extensions/ahp/internal/pi/imageinput.go +224 -0
- package/extensions/ahp/internal/pi/imageinput_test.go +57 -0
- package/extensions/ahp/internal/pi/lifecycle_test.go +381 -0
- package/extensions/ahp/internal/pi/models.go +212 -0
- package/extensions/ahp/internal/pi/models_test.go +128 -0
- package/extensions/ahp/internal/pi/paging.go +95 -0
- package/extensions/ahp/internal/pi/pagingtruncate_test.go +323 -0
- package/extensions/ahp/internal/pi/projecttrust.go +222 -0
- package/extensions/ahp/internal/pi/registry.go +924 -0
- package/extensions/ahp/internal/pi/restart_test.go +185 -0
- package/extensions/ahp/internal/pi/services.go +70 -0
- package/extensions/ahp/internal/pi/sessionconfig.go +64 -0
- package/extensions/ahp/internal/pi/sessionfiles_test.go +74 -0
- package/extensions/ahp/internal/pi/sessionstore.go +83 -0
- package/extensions/ahp/internal/pi/summary_test.go +426 -0
- package/extensions/ahp/internal/pi/workarounds_wire_test.go +208 -0
- package/extensions/ahp/internal/pisession/fromentries_test.go +32 -0
- package/extensions/ahp/internal/pisession/json.go +31 -0
- package/extensions/ahp/internal/pisession/pisession.go +580 -0
- package/extensions/ahp/internal/settings/access.go +63 -0
- package/extensions/ahp/internal/settings/access_test.go +52 -0
- package/extensions/ahp/internal/settings/settings.go +117 -0
- package/extensions/ahp/internal/settings/settings_test.go +104 -0
- package/extensions/ahp/internal/svc/etag_other.go +15 -0
- package/extensions/ahp/internal/svc/etag_unix.go +34 -0
- package/extensions/ahp/internal/svc/glob.go +197 -0
- package/extensions/ahp/internal/svc/mime.go +37 -0
- package/extensions/ahp/internal/svc/paths.go +108 -0
- package/extensions/ahp/internal/svc/pty.go +64 -0
- package/extensions/ahp/internal/svc/pty_darwin.go +42 -0
- package/extensions/ahp/internal/svc/pty_linux.go +36 -0
- package/extensions/ahp/internal/svc/pty_other.go +9 -0
- package/extensions/ahp/internal/svc/pty_test.go +114 -0
- package/extensions/ahp/internal/svc/pty_unix.go +147 -0
- package/extensions/ahp/internal/svc/resource.go +495 -0
- package/extensions/ahp/internal/svc/resource_test.go +501 -0
- package/extensions/ahp/internal/svc/stat_bsd.go +9 -0
- package/extensions/ahp/internal/svc/stat_linux.go +9 -0
- package/extensions/ahp/internal/svc/terminal.go +454 -0
- package/extensions/ahp/internal/svc/terminal_test.go +512 -0
- package/extensions/ahp/internal/svc/watch.go +590 -0
- package/extensions/ahp/internal/svc/watch_test.go +627 -0
- package/extensions/ahp/internal/svc/watchevents_test.go +452 -0
- package/extensions/ahp/internal/svc/watchpolicy.go +81 -0
- package/extensions/ahp/internal/svc/watchpolicy_test.go +151 -0
- package/extensions/ahp/internal/svc/watchracy_test.go +61 -0
- package/extensions/ahp/internal/testkit/schema/LICENSE-agent-host-protocol +21 -0
- package/extensions/ahp/internal/testkit/schema/actions.schema.json +9208 -0
- package/extensions/ahp/internal/testkit/schema/commands.schema.json +10862 -0
- package/extensions/ahp/internal/testkit/schema/errors.schema.json +10928 -0
- package/extensions/ahp/internal/testkit/schema/notifications.schema.json +6667 -0
- package/extensions/ahp/internal/testkit/schema/state.schema.json +6392 -0
- package/extensions/ahp/internal/testkit/schema.go +360 -0
- package/extensions/ahp/internal/testkit/testkit.go +367 -0
- package/extensions/ahp/internal/twin/twin.go +27 -0
- package/extensions/ahp/internal/wire/dispatchable.go +106 -0
- package/extensions/ahp/internal/wire/dispatchable_list.go +15 -0
- package/extensions/ahp/internal/wire/helpers_test.go +5 -0
- package/extensions/ahp/internal/wire/wire.go +218 -0
- package/extensions/ahp/internal/wire/wire_test.go +123 -0
- package/extensions/ahp/internal/ws/helpers_test.go +7 -0
- package/extensions/ahp/internal/ws/vscode_test.go +198 -0
- package/extensions/ahp/internal/ws/ws.go +674 -0
- package/extensions/ahp/internal/ws/ws_test.go +324 -0
- package/extensions/ahp/realpig_test.go +589 -0
- package/extensions/ahp/runtime.go +311 -0
- package/extensions/ahp/runtime_test.go +110 -0
- package/extensions/ahp/testdata/upstream-tests.json +3833 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/LICENSE +21 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/NOTICE-PIGPEN.md +18 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/client.go +1011 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/error.go +111 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/multi_host_state_mirror.go +239 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/reducers.go +1939 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/transport.go +176 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/actions.generated.go +2447 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/commands.generated.go +1546 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/common.go +205 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/errors.generated.go +65 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/messages.generated.go +138 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/notifications.generated.go +264 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/state.generated.go +6171 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/version.generated.go +31 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/go.mod +3 -0
- package/extensions/ahp/twins_test.go +91 -0
- package/package.json +40 -0
- package/proof/PORT.md +121 -0
- package/proof/mutations.json +152 -0
- package/proof/oracle/LICENSE +21 -0
- package/proof/oracle/README.md +120 -0
- package/proof/oracle/UPSTREAM.md +7 -0
- package/proof/oracle/package.json +68 -0
- package/proof/oracle/src/bin/cli.ts +66 -0
- package/proof/oracle/src/bin/tunnel.ts +128 -0
- package/proof/oracle/src/channels/chat.ts +134 -0
- package/proof/oracle/src/channels/root.ts +39 -0
- package/proof/oracle/src/channels/session.ts +118 -0
- package/proof/oracle/src/channels/terminal.ts +11 -0
- package/proof/oracle/src/core/channels.ts +108 -0
- package/proof/oracle/src/core/client-workarounds.ts +358 -0
- package/proof/oracle/src/core/connection.ts +41 -0
- package/proof/oracle/src/core/host.ts +882 -0
- package/proof/oracle/src/core/sequencer.ts +75 -0
- package/proof/oracle/src/core/state-store.ts +162 -0
- package/proof/oracle/src/core/uri.ts +25 -0
- package/proof/oracle/src/host/direct-settings.ts +110 -0
- package/proof/oracle/src/host/pi-host.ts +220 -0
- package/proof/oracle/src/host/serve.ts +71 -0
- package/proof/oracle/src/host/terminal-service.ts +346 -0
- package/proof/oracle/src/pi/activity.ts +171 -0
- package/proof/oracle/src/pi/changeset-service.ts +646 -0
- package/proof/oracle/src/pi/changeset-uri.ts +64 -0
- package/proof/oracle/src/pi/chat-driver.ts +529 -0
- package/proof/oracle/src/pi/completions.ts +160 -0
- package/proof/oracle/src/pi/delete-session.ts +53 -0
- package/proof/oracle/src/pi/event-mapper.ts +648 -0
- package/proof/oracle/src/pi/git-changes.ts +605 -0
- package/proof/oracle/src/pi/history.ts +305 -0
- package/proof/oracle/src/pi/image-input.ts +56 -0
- package/proof/oracle/src/pi/image-mime.ts +8 -0
- package/proof/oracle/src/pi/in-process-backend.ts +155 -0
- package/proof/oracle/src/pi/message-input.ts +212 -0
- package/proof/oracle/src/pi/models.ts +124 -0
- package/proof/oracle/src/pi/project-trust.ts +66 -0
- package/proof/oracle/src/pi/provider.ts +2 -0
- package/proof/oracle/src/pi/resource-paths.ts +80 -0
- package/proof/oracle/src/pi/resource-service.ts +356 -0
- package/proof/oracle/src/pi/resource-watch-policy.ts +35 -0
- package/proof/oracle/src/pi/resource-watch.ts +357 -0
- package/proof/oracle/src/pi/session-catalogue.ts +318 -0
- package/proof/oracle/src/pi/session-config.ts +84 -0
- package/proof/oracle/src/pi/session-history.ts +75 -0
- package/proof/oracle/src/pi/session-hydrator.ts +219 -0
- package/proof/oracle/src/pi/session-registry.ts +815 -0
- package/proof/oracle/src/pi/session-storage.ts +29 -0
- package/proof/oracle/src/pi/session-title.ts +18 -0
- package/proof/oracle/src/pi/turn-paging.ts +94 -0
- package/proof/oracle/src/pi/user-message.ts +66 -0
- package/proof/oracle/src/protocol/errors.ts +44 -0
- package/proof/oracle/src/protocol/jsonrpc.ts +89 -0
- package/proof/oracle/src/protocol/version.ts +39 -0
- package/proof/oracle/src/transport/websocket.ts +136 -0
- package/proof/oracle/src/tunnel/devtunnel.ts +293 -0
- package/proof/oracle/src/tunnel/discovery.ts +37 -0
- package/proof/oracle/test/active-turn-reconnect.test.ts +274 -0
- package/proof/oracle/test/activity.test.ts +171 -0
- package/proof/oracle/test/changeset-lifecycle.test.serial.ts +303 -0
- package/proof/oracle/test/changeset-uri.test.ts +37 -0
- package/proof/oracle/test/changeset.test.serial.ts +487 -0
- package/proof/oracle/test/chat-driver.test.ts +691 -0
- package/proof/oracle/test/client-actions.test.ts +409 -0
- package/proof/oracle/test/client-workarounds.test.ts +320 -0
- package/proof/oracle/test/completions.test.ts +323 -0
- package/proof/oracle/test/delete-session.test.ts +142 -0
- package/proof/oracle/test/direct-settings.test.ts +81 -0
- package/proof/oracle/test/event-mapper.test.ts +619 -0
- package/proof/oracle/test/fetch-turns.test.ts +207 -0
- package/proof/oracle/test/fixtures/abort.json +711 -0
- package/proof/oracle/test/fixtures/bash-long-output.json +1312 -0
- package/proof/oracle/test/fixtures/compaction.json +6285 -0
- package/proof/oracle/test/fixtures/parallel-tools.json +2622 -0
- package/proof/oracle/test/fixtures/plain-text.json +1856 -0
- package/proof/oracle/test/fixtures/single-tool.json +1128 -0
- package/proof/oracle/test/fixtures/steering.json +3926 -0
- package/proof/oracle/test/fixtures/tool-bash.json +2372 -0
- package/proof/oracle/test/fixtures/tool-edit.json +3983 -0
- package/proof/oracle/test/fixtures/tool-error.json +2850 -0
- package/proof/oracle/test/fixtures/tool-find.json +1162 -0
- package/proof/oracle/test/fixtures/tool-grep.json +2783 -0
- package/proof/oracle/test/fixtures/tool-loop.json +1693 -0
- package/proof/oracle/test/fixtures/tool-ls.json +2427 -0
- package/proof/oracle/test/fixtures/tool-write.json +1271 -0
- package/proof/oracle/test/handshake.test.ts +275 -0
- package/proof/oracle/test/harness.ts +153 -0
- package/proof/oracle/test/hydrated-session-lifecycle.test.ts +211 -0
- package/proof/oracle/test/image-input.test.ts +28 -0
- package/proof/oracle/test/image-session.test.ts +69 -0
- package/proof/oracle/test/live-turn.test.ts +304 -0
- package/proof/oracle/test/mapper-fixtures.test.ts +293 -0
- package/proof/oracle/test/message-input.test.ts +181 -0
- package/proof/oracle/test/model-discovery.test.ts +170 -0
- package/proof/oracle/test/models.test.ts +109 -0
- package/proof/oracle/test/pi-host.test.ts +91 -0
- package/proof/oracle/test/pi-replay.test.ts +141 -0
- package/proof/oracle/test/project-trust.test.ts +125 -0
- package/proof/oracle/test/protocol-surface.test.ts +121 -0
- package/proof/oracle/test/pty.test.ts +100 -0
- package/proof/oracle/test/reconnect.test.ts +389 -0
- package/proof/oracle/test/resource-watch-policy.test.ts +78 -0
- package/proof/oracle/test/resource-watch.test.serial.ts +536 -0
- package/proof/oracle/test/resource.test.ts +499 -0
- package/proof/oracle/test/schema.test.ts +88 -0
- package/proof/oracle/test/session-catalogue.test.ts +314 -0
- package/proof/oracle/test/session-config.test.ts +224 -0
- package/proof/oracle/test/session-disposal.test.ts +364 -0
- package/proof/oracle/test/session-hydration.test.ts +194 -0
- package/proof/oracle/test/session-lifecycle.test.ts +416 -0
- package/proof/oracle/test/session-storage.test.serial.ts +32 -0
- package/proof/oracle/test/session-summary.test.ts +418 -0
- package/proof/oracle/test/subscriptions.test.ts +198 -0
- package/proof/oracle/test/support/assertions.ts +32 -0
- package/proof/oracle/test/support/async.ts +26 -0
- package/proof/oracle/test/support/hydrated-session.ts +205 -0
- package/proof/oracle/test/support/images.ts +8 -0
- package/proof/oracle/test/support/recorded-fixtures.ts +60 -0
- package/proof/oracle/test/support/recorded-scenarios.ts +145 -0
- package/proof/oracle/test/support/replay.ts +194 -0
- package/proof/oracle/test/support/schema.ts +221 -0
- package/proof/oracle/test/support/session-files.ts +12 -0
- package/proof/oracle/test/support/session-storage.ts +22 -0
- package/proof/oracle/test/support/upstream.ts +25 -0
- package/proof/oracle/test/support/watch-events.ts +108 -0
- package/proof/oracle/test/terminal-service.test.ts +495 -0
- package/proof/oracle/test/truncate.test.ts +230 -0
- package/proof/oracle/test/tunnel.test.ts +258 -0
- package/proof/oracle/test/upstream-workarounds.test.ts +37 -0
- package/proof/oracle/test/uri.test.ts +24 -0
- package/proof/oracle/test/watch-events.test.ts +118 -0
- package/proof/oracle/tsconfig.json +32 -0
- package/proof/proof-piglet/piglet.yaml +18 -0
- package/proof/tools/fakellm/go.mod +3 -0
- package/proof/tools/fakellm/main.go +109 -0
- package/proof/tools/gen-dispatchable.py +30 -0
- package/provenance.json +28 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
package mapper_test
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"regexp"
|
|
9
|
+
"sort"
|
|
10
|
+
"strings"
|
|
11
|
+
"testing"
|
|
12
|
+
|
|
13
|
+
"github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
|
|
14
|
+
|
|
15
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/mapper"
|
|
16
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/twin"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
// recorded is one captured pi event stream (upstream test/fixtures/*.json, recorded from a real
|
|
20
|
+
// model and scrubbed).
|
|
21
|
+
type recorded struct {
|
|
22
|
+
Name string `json:"name"`
|
|
23
|
+
Description string `json:"description"`
|
|
24
|
+
Prompt string `json:"prompt"`
|
|
25
|
+
Events []obj `json:"events"`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// expectedFixtures is the manifest of upstream's recorded corpus: a missing, extra or renamed
|
|
29
|
+
// fixture fails the load, as in upstream's loadRecordedFixtures.
|
|
30
|
+
var expectedFixtures = []string{
|
|
31
|
+
"abort", "bash-long-output", "compaction", "parallel-tools", "plain-text", "single-tool", "steering",
|
|
32
|
+
"tool-bash", "tool-edit", "tool-error", "tool-find", "tool-grep", "tool-loop", "tool-ls", "tool-write",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func loadRecorded(t testing.TB) []recorded {
|
|
36
|
+
t.Helper()
|
|
37
|
+
files, err := filepath.Glob("testdata/fixtures/*.json")
|
|
38
|
+
if err != nil {
|
|
39
|
+
t.Fatal(err)
|
|
40
|
+
}
|
|
41
|
+
var names []string
|
|
42
|
+
for _, f := range files {
|
|
43
|
+
names = append(names, strings.TrimSuffix(filepath.Base(f), ".json"))
|
|
44
|
+
}
|
|
45
|
+
sort.Strings(names)
|
|
46
|
+
want := append([]string(nil), expectedFixtures...)
|
|
47
|
+
sort.Strings(want)
|
|
48
|
+
if fmt.Sprint(names) != fmt.Sprint(want) {
|
|
49
|
+
t.Fatalf("recorded fixture set differs from the manifest; expected=%v actual=%v", want, names)
|
|
50
|
+
}
|
|
51
|
+
var out []recorded
|
|
52
|
+
for _, n := range expectedFixtures {
|
|
53
|
+
raw, err := os.ReadFile(filepath.Join("testdata/fixtures", n+".json"))
|
|
54
|
+
if err != nil {
|
|
55
|
+
t.Fatal(err)
|
|
56
|
+
}
|
|
57
|
+
var r recorded
|
|
58
|
+
if err := json.Unmarshal(raw, &r); err != nil {
|
|
59
|
+
t.Fatalf("%s: %v", n, err)
|
|
60
|
+
}
|
|
61
|
+
if r.Name != n || r.Prompt == "" || len(r.Events) == 0 {
|
|
62
|
+
t.Fatalf("%s: malformed fixture", n)
|
|
63
|
+
}
|
|
64
|
+
out = append(out, r)
|
|
65
|
+
}
|
|
66
|
+
return out
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func replayFixture(t testing.TB, r recorded) replayed {
|
|
70
|
+
t.Helper()
|
|
71
|
+
m := mapper.NewTurnMapper("replay-turn", 0)
|
|
72
|
+
actions := []ahptypes.StateAction{mapper.UserTurnStarted("replay-turn", r.Prompt, "1970-01-01T00:00:00.000Z")}
|
|
73
|
+
for _, e := range r.Events {
|
|
74
|
+
actions = append(actions, m.Handle(e)...)
|
|
75
|
+
}
|
|
76
|
+
return replayed{actions: actions, state: reduce(t, actions)}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func markdownText(state obj) string {
|
|
80
|
+
return strings.Join(contents(partsOfKind(state, "markdown")), "")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func toolResultText(call obj) string {
|
|
84
|
+
var sb strings.Builder
|
|
85
|
+
for _, b := range list(call, "content") {
|
|
86
|
+
if str(b, "type") == "text" {
|
|
87
|
+
sb.WriteString(str(b, "text"))
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return sb.String()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func assertReplayInvariants(t *testing.T, r recorded, rep replayed) {
|
|
94
|
+
t.Helper()
|
|
95
|
+
opened := len(ofType(rep.actions, "chat/turnStarted"))
|
|
96
|
+
terminators := len(ofType(rep.actions, "chat/turnComplete")) + len(ofType(rep.actions, "chat/turnCancelled")) + len(ofType(rep.actions, "chat/error"))
|
|
97
|
+
// More than one turn means pi injected a message mid-run; each still has to terminate exactly once.
|
|
98
|
+
if terminators != opened {
|
|
99
|
+
t.Fatalf("%s: every opened turn must terminate once (%d opened, %d terminated)", r.Name, opened, terminators)
|
|
100
|
+
}
|
|
101
|
+
if rep.state["activeTurn"] != nil {
|
|
102
|
+
t.Fatalf("%s: active turn remained after replay", r.Name)
|
|
103
|
+
}
|
|
104
|
+
if n := len(list(rep.state, "turns")); n != opened {
|
|
105
|
+
t.Fatalf("%s: reduced turn count %d differs from opened turns %d", r.Name, n, opened)
|
|
106
|
+
}
|
|
107
|
+
assertActionsValid(t, rep.actions, r.Name)
|
|
108
|
+
// Every tool call ends completed or cancelled: none is left running.
|
|
109
|
+
for _, turn := range list(rep.state, "turns") {
|
|
110
|
+
for _, p := range list(turn, "responseParts") {
|
|
111
|
+
if str(p, "kind") == "toolCall" {
|
|
112
|
+
if st := str(p, "toolCall", "status"); st != "completed" && st != "cancelled" {
|
|
113
|
+
t.Fatalf("%s: %s ended in %s", r.Name, str(p, "toolCall", "toolName"), st)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// A delta naming an unknown partId is a silent reducer no-op, so validate creation order as
|
|
119
|
+
// well as uniqueness.
|
|
120
|
+
created := map[string]bool{}
|
|
121
|
+
var ids []string
|
|
122
|
+
for _, a := range rep.actions {
|
|
123
|
+
o := asObj(a)
|
|
124
|
+
switch o["type"] {
|
|
125
|
+
case "chat/responsePart":
|
|
126
|
+
if id := str(o, "part", "id"); id != "" {
|
|
127
|
+
ids = append(ids, id)
|
|
128
|
+
created[id] = true
|
|
129
|
+
}
|
|
130
|
+
case "chat/delta", "chat/reasoning":
|
|
131
|
+
if !created[str(o, "partId")] {
|
|
132
|
+
t.Fatalf("%s: delta targets unknown part %s", r.Name, str(o, "partId"))
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
seen := map[string]bool{}
|
|
137
|
+
for _, id := range ids {
|
|
138
|
+
if seen[id] {
|
|
139
|
+
t.Fatalf("%s: response part ids collided within a turn: %s", r.Name, id)
|
|
140
|
+
}
|
|
141
|
+
seen[id] = true
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Twins of upstream test/mapper-fixtures.test.ts.
|
|
146
|
+
func TestRecordedStreamReplay(t *testing.T) {
|
|
147
|
+
fixtures := loadRecorded(t)
|
|
148
|
+
byName := map[string]replayed{}
|
|
149
|
+
src := map[string]recorded{}
|
|
150
|
+
for _, r := range fixtures {
|
|
151
|
+
byName[r.Name] = replayFixture(t, r)
|
|
152
|
+
src[r.Name] = r
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// One literal twin per recorded stream (the twin gate reads titles from source).
|
|
156
|
+
twin.Run(t, "mapper-fixtures", "plain-text — A reply with no tool calls — the simplest possible turn.", func(t *testing.T) {
|
|
157
|
+
assertReplayInvariants(t, src["plain-text"], byName["plain-text"])
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
twin.Run(t, "mapper-fixtures", "single-tool — One tool call, then an answer derived from its result.", func(t *testing.T) {
|
|
161
|
+
assertReplayInvariants(t, src["single-tool"], byName["single-tool"])
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
twin.Run(t, "mapper-fixtures", "parallel-tools — Several tool calls in one assistant message — exercises contentIndex fan-out.", func(t *testing.T) {
|
|
165
|
+
assertReplayInvariants(t, src["parallel-tools"], byName["parallel-tools"])
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
twin.Run(t, "mapper-fixtures", "tool-loop — Two or more assistant messages in one turn — the case where contentIndex restarts and partIds must not collide.", func(t *testing.T) {
|
|
169
|
+
assertReplayInvariants(t, src["tool-loop"], byName["tool-loop"])
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
twin.Run(t, "mapper-fixtures", "tool-error — A failing tool call the agent has to recover from.", func(t *testing.T) {
|
|
173
|
+
assertReplayInvariants(t, src["tool-error"], byName["tool-error"])
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
twin.Run(t, "mapper-fixtures", "abort — A turn cancelled while it is running.", func(t *testing.T) {
|
|
177
|
+
assertReplayInvariants(t, src["abort"], byName["abort"])
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
twin.Run(t, "mapper-fixtures", "steering — A steering message injected into a running turn.", func(t *testing.T) {
|
|
181
|
+
assertReplayInvariants(t, src["steering"], byName["steering"])
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
twin.Run(t, "mapper-fixtures", "tool-edit — An `edit` call — the one tool whose result carries a diff and a patch.", func(t *testing.T) {
|
|
185
|
+
assertReplayInvariants(t, src["tool-edit"], byName["tool-edit"])
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
twin.Run(t, "mapper-fixtures", "tool-write — A `write` call — a whole new file rather than an edit to one.", func(t *testing.T) {
|
|
189
|
+
assertReplayInvariants(t, src["tool-write"], byName["tool-write"])
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
twin.Run(t, "mapper-fixtures", "tool-bash — A `bash` call — stdout, exit status, and pi's truncation metadata.", func(t *testing.T) {
|
|
193
|
+
assertReplayInvariants(t, src["tool-bash"], byName["tool-bash"])
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
twin.Run(t, "mapper-fixtures", "tool-ls — An `ls` call — a directory listing, which reports its own entry limit.", func(t *testing.T) {
|
|
197
|
+
assertReplayInvariants(t, src["tool-ls"], byName["tool-ls"])
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
twin.Run(t, "mapper-fixtures", "tool-grep — A `grep` call — match counts and line truncation live in its details.", func(t *testing.T) {
|
|
201
|
+
assertReplayInvariants(t, src["tool-grep"], byName["tool-grep"])
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
twin.Run(t, "mapper-fixtures", "tool-find — A `find` call — path globbing, with its own result limit.", func(t *testing.T) {
|
|
205
|
+
assertReplayInvariants(t, src["tool-find"], byName["tool-find"])
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
twin.Run(t, "mapper-fixtures", "compaction — A manual compaction — `buildContextEntries` starts from the newest one, so this is the boundary history rebuilding and turn paging are built around.", func(t *testing.T) {
|
|
209
|
+
assertReplayInvariants(t, src["compaction"], byName["compaction"])
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
twin.Run(t, "mapper-fixtures", "bash-long-output — A bash call whose output is large enough for pi to stream updates and report truncation.", func(t *testing.T) {
|
|
213
|
+
assertReplayInvariants(t, src["bash-long-output"], byName["bash-long-output"])
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
twin.Run(t, "mapper-fixtures", "plain-text: answers with no tool calls", func(t *testing.T) {
|
|
217
|
+
st := byName["plain-text"].state
|
|
218
|
+
if got := str(st, "turns", 0, "state"); got != "complete" {
|
|
219
|
+
t.Fatal(got)
|
|
220
|
+
}
|
|
221
|
+
if n := len(toolCalls(st)); n != 0 {
|
|
222
|
+
t.Fatalf("tool calls = %d", n)
|
|
223
|
+
}
|
|
224
|
+
if !regexp.MustCompile(`(?i)PONG`).MatchString(markdownText(st)) {
|
|
225
|
+
t.Fatal(markdownText(st))
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
twin.Run(t, "mapper-fixtures", "describes a tool call in terms a user can act on", func(t *testing.T) {
|
|
230
|
+
// A heading, a line while it runs and a line once it has: a message that only repeats the
|
|
231
|
+
// tool's name spends all three on saying `read` three times.
|
|
232
|
+
for _, r := range fixtures {
|
|
233
|
+
for _, turn := range list(byName[r.Name].state, "turns") {
|
|
234
|
+
for _, p := range list(turn, "responseParts") {
|
|
235
|
+
call, _ := get(p, "toolCall").(obj)
|
|
236
|
+
if call == nil {
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
if in := str(call, "toolInput"); in == "" || in == "{}" {
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
if str(call, "invocationMessage") == str(call, "toolName") {
|
|
243
|
+
t.Fatalf("%s: %s announces itself with nothing but its own name", r.Name, str(call, "toolName"))
|
|
244
|
+
}
|
|
245
|
+
// Identical text leaves a completed call still claiming to be running.
|
|
246
|
+
if str(call, "pastTenseMessage") == str(call, "invocationMessage") {
|
|
247
|
+
t.Fatalf("%s: %s still says it is %s after it finished", r.Name, str(call, "toolName"), str(call, "invocationMessage"))
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
twin.Run(t, "mapper-fixtures", "shows a tool's subject rather than its arguments as JSON", func(t *testing.T) {
|
|
255
|
+
for _, r := range fixtures {
|
|
256
|
+
for _, turn := range list(byName[r.Name].state, "turns") {
|
|
257
|
+
for _, p := range list(turn, "responseParts") {
|
|
258
|
+
call, _ := get(p, "toolCall").(obj)
|
|
259
|
+
if call == nil {
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
if in, ok := call["toolInput"].(string); ok && strings.HasPrefix(in, "{") {
|
|
263
|
+
t.Fatalf("%s: %s shows its arguments as JSON: %.60s", r.Name, str(call, "toolName"), in)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
twin.Run(t, "mapper-fixtures", "tool-edit: hands the client the patch, not a count of edited blocks", func(t *testing.T) {
|
|
271
|
+
var edits []obj
|
|
272
|
+
for _, c := range toolCalls(byName["tool-edit"].state) {
|
|
273
|
+
if str(c, "toolName") == "edit" {
|
|
274
|
+
edits = append(edits, c)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if len(edits) < 1 {
|
|
278
|
+
t.Fatal("expected an edit call")
|
|
279
|
+
}
|
|
280
|
+
for _, c := range edits {
|
|
281
|
+
shown := toolResultText(c)
|
|
282
|
+
if !regexp.MustCompile(`^--- |\n--- `).MatchString(shown) {
|
|
283
|
+
t.Fatalf("edit result carries no patch: %.80s", shown)
|
|
284
|
+
}
|
|
285
|
+
if !regexp.MustCompile(`(?m)^\+.*$`).MatchString(shown) {
|
|
286
|
+
t.Fatal("a patch with no added lines is not a patch")
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
twin.Run(t, "mapper-fixtures", "single-tool: runs one tool and answers from its result", func(t *testing.T) {
|
|
292
|
+
st := byName["single-tool"].state
|
|
293
|
+
if got := str(st, "turns", 0, "state"); got != "complete" {
|
|
294
|
+
t.Fatal(got)
|
|
295
|
+
}
|
|
296
|
+
if len(toolCalls(st)) < 1 {
|
|
297
|
+
t.Fatal("no tool call")
|
|
298
|
+
}
|
|
299
|
+
if !strings.Contains(markdownText(st), "ALPHA BETA GAMMA") {
|
|
300
|
+
t.Fatal(markdownText(st))
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
twin.Run(t, "mapper-fixtures", "parallel-tools: keeps several tool calls in one message distinct", func(t *testing.T) {
|
|
305
|
+
st := byName["parallel-tools"].state
|
|
306
|
+
if n := len(toolCalls(st)); n < 2 {
|
|
307
|
+
t.Fatalf("expected multiple tool calls, got %d", n)
|
|
308
|
+
}
|
|
309
|
+
if !strings.Contains(markdownText(st), "FIRST") || !strings.Contains(markdownText(st), "SECOND") {
|
|
310
|
+
t.Fatal(markdownText(st))
|
|
311
|
+
}
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
twin.Run(t, "mapper-fixtures", "tool-loop: spans several assistant messages without part collisions", func(t *testing.T) {
|
|
315
|
+
assistant := 0
|
|
316
|
+
for _, e := range src["tool-loop"].Events {
|
|
317
|
+
if e["type"] == "message_start" && str(e, "message", "role") == "assistant" {
|
|
318
|
+
assistant++
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// The scenario exists to produce more than one assistant message, where contentIndex restarts at 0.
|
|
322
|
+
if assistant < 2 {
|
|
323
|
+
t.Fatalf("expected multiple assistant messages, got %d", assistant)
|
|
324
|
+
}
|
|
325
|
+
st := byName["tool-loop"].state
|
|
326
|
+
if got := str(st, "turns", 0, "state"); got != "complete" {
|
|
327
|
+
t.Fatal(got)
|
|
328
|
+
}
|
|
329
|
+
if !strings.Contains(markdownText(st), "42") {
|
|
330
|
+
t.Fatal(markdownText(st))
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
twin.Run(t, "mapper-fixtures", "tool-error: surfaces a failed tool without failing the turn", func(t *testing.T) {
|
|
335
|
+
st := byName["tool-error"].state
|
|
336
|
+
var failed []obj
|
|
337
|
+
for _, c := range toolCalls(st) {
|
|
338
|
+
if c["success"] == false {
|
|
339
|
+
failed = append(failed, c)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if len(failed) < 1 {
|
|
343
|
+
t.Fatal("expected a failed tool call")
|
|
344
|
+
}
|
|
345
|
+
// What the client shows for a failure is error.message: pi says why (an ENOENT naming the
|
|
346
|
+
// path); repeating the tool's name tells the user nothing.
|
|
347
|
+
for _, c := range failed {
|
|
348
|
+
reported := toolResultText(c)
|
|
349
|
+
if reported == "" {
|
|
350
|
+
t.Fatalf("%s: no failure text to show", str(c, "toolName"))
|
|
351
|
+
}
|
|
352
|
+
if got := str(c, "error", "message"); got != reported {
|
|
353
|
+
t.Fatalf("%s: error.message %q should carry what pi reported %q", str(c, "toolName"), got, reported)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if got := str(st, "turns", 0, "state"); got != "complete" {
|
|
357
|
+
t.Fatal(got)
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
twin.Run(t, "mapper-fixtures", "abort: ends the turn as cancelled", func(t *testing.T) {
|
|
362
|
+
if got := str(byName["abort"].state, "turns", 0, "state"); got != "cancelled" {
|
|
363
|
+
t.Fatal(got)
|
|
364
|
+
}
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
twin.Run(t, "mapper-fixtures", "steering: the injected message becomes its own turn", func(t *testing.T) {
|
|
368
|
+
injected := 0
|
|
369
|
+
for _, e := range src["steering"].Events {
|
|
370
|
+
if e["type"] == "message_start" && str(e, "message", "role") == "user" {
|
|
371
|
+
injected++
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if injected < 2 {
|
|
375
|
+
t.Fatal("expected the steering message to appear mid-run")
|
|
376
|
+
}
|
|
377
|
+
st := byName["steering"].state
|
|
378
|
+
// pi stores an injected message as an ordinary user message, so a rebuild from disk makes it
|
|
379
|
+
// a turn; the live path matches.
|
|
380
|
+
if n := len(list(st, "turns")); n != injected {
|
|
381
|
+
t.Fatalf("turns = %d, injected = %d", n, injected)
|
|
382
|
+
}
|
|
383
|
+
if !strings.Contains(str(st, "turns", -1, "message", "text"), "Stop counting") {
|
|
384
|
+
t.Fatal(str(st, "turns", -1, "message", "text"))
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
package mapper_test
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
|
|
9
|
+
"github.com/microsoft/agent-host-protocol/clients/go/ahp"
|
|
10
|
+
"github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
|
|
11
|
+
|
|
12
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/channels"
|
|
13
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/mapper"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// obj is a decoded JSON object; the pi events and the reduced chat state are compared as data,
|
|
17
|
+
// the way upstream's tests compare plain objects.
|
|
18
|
+
type obj = map[string]any
|
|
19
|
+
|
|
20
|
+
const (
|
|
21
|
+
chatURI = "ahp-chat:/c1"
|
|
22
|
+
turnID = "turn-1"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
// pi mirrors the `pi` shorthand object of upstream's event-mapper.test.ts.
|
|
26
|
+
var pi = struct {
|
|
27
|
+
agentStart, settled, assistantStart func() obj
|
|
28
|
+
agentEnd func(willRetry bool) obj
|
|
29
|
+
assistantEnd func(usage obj) obj
|
|
30
|
+
text, thinking func(idx int, delta string) obj
|
|
31
|
+
textEnd, thinkingEnd func(idx int, content string) obj
|
|
32
|
+
toolStart func(idx int, id, name string) obj
|
|
33
|
+
toolDelta func(idx int, delta string) obj
|
|
34
|
+
toolEnd func(idx int, id, name string, args any) obj
|
|
35
|
+
execUpdate func(id, name, text string) obj
|
|
36
|
+
execEnd func(id, name, text string, isError bool) obj
|
|
37
|
+
streamError func(message string, aborted bool) obj
|
|
38
|
+
}{
|
|
39
|
+
agentStart: func() obj { return obj{"type": "agent_start"} },
|
|
40
|
+
agentEnd: func(w bool) obj { return obj{"type": "agent_end", "messages": []any{}, "willRetry": w} },
|
|
41
|
+
settled: func() obj { return obj{"type": "agent_settled"} },
|
|
42
|
+
assistantStart: func() obj {
|
|
43
|
+
return obj{"type": "message_start", "message": obj{"role": "assistant", "content": []any{}}}
|
|
44
|
+
},
|
|
45
|
+
assistantEnd: func(usage obj) obj {
|
|
46
|
+
msg := obj{"role": "assistant", "content": []any{}}
|
|
47
|
+
if usage != nil {
|
|
48
|
+
msg["usage"] = usage
|
|
49
|
+
msg["model"] = "test-model"
|
|
50
|
+
}
|
|
51
|
+
return obj{"type": "message_end", "message": msg}
|
|
52
|
+
},
|
|
53
|
+
text: func(i int, d string) obj { return update(obj{"type": "text_delta", "contentIndex": i, "delta": d}) },
|
|
54
|
+
thinking: func(i int, d string) obj { return update(obj{"type": "thinking_delta", "contentIndex": i, "delta": d}) },
|
|
55
|
+
textEnd: func(i int, c string) obj { return update(obj{"type": "text_end", "contentIndex": i, "content": c}) },
|
|
56
|
+
thinkingEnd: func(i int, c string) obj { return update(obj{"type": "thinking_end", "contentIndex": i, "content": c}) },
|
|
57
|
+
toolStart: func(i int, id, name string) obj {
|
|
58
|
+
content := make([]any, i+1)
|
|
59
|
+
for k := range content {
|
|
60
|
+
content[k] = obj{}
|
|
61
|
+
}
|
|
62
|
+
content[i] = obj{"type": "toolCall", "id": id, "name": name}
|
|
63
|
+
return update(obj{"type": "toolcall_start", "contentIndex": i, "partial": obj{"content": content}})
|
|
64
|
+
},
|
|
65
|
+
toolDelta: func(i int, d string) obj { return update(obj{"type": "toolcall_delta", "contentIndex": i, "delta": d}) },
|
|
66
|
+
toolEnd: func(i int, id, name string, args any) obj {
|
|
67
|
+
return update(obj{"type": "toolcall_end", "contentIndex": i, "toolCall": obj{"id": id, "name": name, "arguments": args}})
|
|
68
|
+
},
|
|
69
|
+
execUpdate: func(id, name, text string) obj {
|
|
70
|
+
return obj{"type": "tool_execution_update", "toolCallId": id, "toolName": name,
|
|
71
|
+
"partialResult": obj{"content": []any{obj{"type": "text", "text": text}}}}
|
|
72
|
+
},
|
|
73
|
+
execEnd: func(id, name, text string, isError bool) obj {
|
|
74
|
+
return obj{"type": "tool_execution_end", "toolCallId": id, "toolName": name,
|
|
75
|
+
"result": obj{"content": []any{obj{"type": "text", "text": text}}}, "isError": isError}
|
|
76
|
+
},
|
|
77
|
+
streamError: func(message string, aborted bool) obj {
|
|
78
|
+
reason := "error"
|
|
79
|
+
if aborted {
|
|
80
|
+
reason = "aborted"
|
|
81
|
+
}
|
|
82
|
+
return update(obj{"type": "error", "reason": reason, "error": obj{"errorMessage": message, "stopReason": reason}})
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func update(ame obj) obj {
|
|
87
|
+
return obj{"type": "message_update", "message": obj{"role": "assistant"}, "assistantMessageEvent": ame}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// replayed is the outcome of running a turn end to end.
|
|
91
|
+
type replayed struct {
|
|
92
|
+
actions []ahptypes.StateAction
|
|
93
|
+
state obj // the reduced ChatState as JSON data
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// reduce applies actions to a fresh chat with the protocol's own reducer.
|
|
97
|
+
func reduce(t testing.TB, actions []ahptypes.StateAction) obj {
|
|
98
|
+
t.Helper()
|
|
99
|
+
state := channels.InitialChatState(chatURI, "Test chat", nil)
|
|
100
|
+
for _, a := range actions {
|
|
101
|
+
ahp.ApplyActionToChat(state, a)
|
|
102
|
+
}
|
|
103
|
+
return toObj(t, state)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
func toObj(t testing.TB, v any) obj {
|
|
107
|
+
t.Helper()
|
|
108
|
+
raw, err := json.Marshal(v)
|
|
109
|
+
if err != nil {
|
|
110
|
+
t.Fatal(err)
|
|
111
|
+
}
|
|
112
|
+
var out obj
|
|
113
|
+
if err := json.Unmarshal(raw, &out); err != nil {
|
|
114
|
+
t.Fatal(err)
|
|
115
|
+
}
|
|
116
|
+
return out
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
func actionType(a ahptypes.StateAction) string { return string(asObj(a)["type"].(string)) }
|
|
120
|
+
|
|
121
|
+
func asObj(v any) obj {
|
|
122
|
+
raw, err := json.Marshal(v)
|
|
123
|
+
if err != nil {
|
|
124
|
+
panic(err)
|
|
125
|
+
}
|
|
126
|
+
var out obj
|
|
127
|
+
if err := json.Unmarshal(raw, &out); err != nil {
|
|
128
|
+
panic(err)
|
|
129
|
+
}
|
|
130
|
+
return out
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
func ofType(actions []ahptypes.StateAction, typ string) []obj {
|
|
134
|
+
var out []obj
|
|
135
|
+
for _, a := range actions {
|
|
136
|
+
if o := asObj(a); o["type"] == typ {
|
|
137
|
+
out = append(out, o)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return out
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func newMapper() *mapper.TurnMapper { return mapper.NewTurnMapper(turnID, 0) }
|
|
144
|
+
|
|
145
|
+
// runTurn feeds events through a mapper and reduces the result, like upstream's runTurn.
|
|
146
|
+
func runTurn(t testing.TB, events []obj, text ...string) replayed {
|
|
147
|
+
t.Helper()
|
|
148
|
+
prompt := "Hello"
|
|
149
|
+
if len(text) > 0 {
|
|
150
|
+
prompt = text[0]
|
|
151
|
+
}
|
|
152
|
+
m := newMapper()
|
|
153
|
+
actions := []ahptypes.StateAction{mapper.UserTurnStarted(turnID, prompt, "1970-01-01T00:00:00.000Z")}
|
|
154
|
+
for _, e := range events {
|
|
155
|
+
actions = append(actions, m.Handle(e)...)
|
|
156
|
+
}
|
|
157
|
+
return replayed{actions: actions, state: reduce(t, actions)}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// get walks decoded JSON: strings index objects, ints index arrays.
|
|
161
|
+
func get(v any, path ...any) any {
|
|
162
|
+
for _, p := range path {
|
|
163
|
+
switch k := p.(type) {
|
|
164
|
+
case string:
|
|
165
|
+
m, ok := v.(obj)
|
|
166
|
+
if !ok {
|
|
167
|
+
return nil
|
|
168
|
+
}
|
|
169
|
+
v = m[k]
|
|
170
|
+
case int:
|
|
171
|
+
a, ok := v.([]any)
|
|
172
|
+
if !ok {
|
|
173
|
+
return nil
|
|
174
|
+
}
|
|
175
|
+
if k < 0 {
|
|
176
|
+
k += len(a)
|
|
177
|
+
}
|
|
178
|
+
if k < 0 || k >= len(a) {
|
|
179
|
+
return nil
|
|
180
|
+
}
|
|
181
|
+
v = a[k]
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return v
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
func list(v any, path ...any) []any {
|
|
188
|
+
l, _ := get(v, path...).([]any)
|
|
189
|
+
return l
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
func str(v any, path ...any) string {
|
|
193
|
+
s, _ := get(v, path...).(string)
|
|
194
|
+
return s
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// partsOfKind returns the response parts of the first (or active) turn with that kind.
|
|
198
|
+
func partsOfKind(state obj, kind string) []obj {
|
|
199
|
+
turn := get(state, "turns", 0)
|
|
200
|
+
if turn == nil {
|
|
201
|
+
turn = get(state, "activeTurn")
|
|
202
|
+
}
|
|
203
|
+
var out []obj
|
|
204
|
+
for _, p := range list(turn, "responseParts") {
|
|
205
|
+
if str(p, "kind") == kind {
|
|
206
|
+
out = append(out, p.(obj))
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return out
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
func contents(parts []obj) []string {
|
|
213
|
+
out := []string{}
|
|
214
|
+
for _, p := range parts {
|
|
215
|
+
out = append(out, str(p, "content"))
|
|
216
|
+
}
|
|
217
|
+
return out
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
func toolCalls(state obj) []obj {
|
|
221
|
+
var out []obj
|
|
222
|
+
for _, p := range partsOfKind(state, "toolCall") {
|
|
223
|
+
out = append(out, p["toolCall"].(obj))
|
|
224
|
+
}
|
|
225
|
+
return out
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
func equal(t testing.TB, got, want any, what string) {
|
|
229
|
+
t.Helper()
|
|
230
|
+
g, _ := json.Marshal(got)
|
|
231
|
+
w, _ := json.Marshal(want)
|
|
232
|
+
if string(g) != string(w) {
|
|
233
|
+
t.Fatalf("%s:\n got %s\n want %s", what, g, w)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
func assertActionsValid(t testing.TB, actions []ahptypes.StateAction, context string) {
|
|
238
|
+
t.Helper()
|
|
239
|
+
for _, a := range actions {
|
|
240
|
+
if err := checkAction(a); err != nil {
|
|
241
|
+
t.Fatalf("%s: non-conforming %s: %v", context, actionType(a), err)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
func fail(t testing.TB, format string, args ...any) {
|
|
247
|
+
t.Helper()
|
|
248
|
+
t.Fatal(fmt.Sprintf(format, args...))
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
var _ = strings.TrimSpace
|