@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,380 @@
|
|
|
1
|
+
package mapper_test
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/base64"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
|
|
8
|
+
|
|
9
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/mapper"
|
|
10
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/twin"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// onePixelPNG is upstream test/support/images.ts ONE_PIXEL_PNG.
|
|
14
|
+
const onePixelPNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
|
|
15
|
+
|
|
16
|
+
func turnErrorMessage(state obj, turn int) string {
|
|
17
|
+
last := get(state, "turns", turn, "responseParts", -1)
|
|
18
|
+
if str(last, "kind") != "error" {
|
|
19
|
+
return ""
|
|
20
|
+
}
|
|
21
|
+
return str(last, "error", "message")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func statusHas(state obj, bit float64) bool {
|
|
25
|
+
f, _ := state["status"].(float64)
|
|
26
|
+
return int(f)&int(bit) != 0
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Twins of upstream test/event-mapper.test.ts.
|
|
30
|
+
func TestEventMapperTurnBoundary(t *testing.T) {
|
|
31
|
+
|
|
32
|
+
twin.Run(t, "event-mapper", "spans several agent runs in a single turn", func(t *testing.T) {
|
|
33
|
+
// An auto-retry produces a second agent_start/agent_end pair. Closing the turn on the first
|
|
34
|
+
// agent_end would make every later action a silent no-op in the reducer.
|
|
35
|
+
r := runTurn(t, []obj{
|
|
36
|
+
pi.agentStart(), pi.assistantStart(), pi.text(0, "first attempt"), pi.agentEnd(true),
|
|
37
|
+
pi.agentStart(), pi.assistantStart(), pi.text(0, "second attempt"), pi.assistantEnd(nil), pi.agentEnd(false),
|
|
38
|
+
pi.settled(),
|
|
39
|
+
})
|
|
40
|
+
if r.state["activeTurn"] != nil {
|
|
41
|
+
t.Fatal("turn still active")
|
|
42
|
+
}
|
|
43
|
+
if n := len(list(r.state, "turns")); n != 1 {
|
|
44
|
+
t.Fatalf("turns = %d", n)
|
|
45
|
+
}
|
|
46
|
+
if got := str(r.state, "turns", 0, "state"); got != "complete" {
|
|
47
|
+
t.Fatalf("state = %q", got)
|
|
48
|
+
}
|
|
49
|
+
// Both attempts survive as separate parts: the protocol has no action that removes one.
|
|
50
|
+
equal(t, contents(partsOfKind(r.state, "markdown")), []string{"first attempt", "second attempt"}, "markdown")
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
twin.Run(t, "event-mapper", "only closes the turn on agent_settled", func(t *testing.T) {
|
|
54
|
+
m := newMapper()
|
|
55
|
+
var before = 0
|
|
56
|
+
for _, e := range []obj{pi.agentStart(), pi.assistantStart(), pi.text(0, "hi"), pi.agentEnd(false)} {
|
|
57
|
+
before += len(ofType(m.Handle(e), "chat/turnComplete"))
|
|
58
|
+
}
|
|
59
|
+
if before != 0 {
|
|
60
|
+
t.Fatal("the turn closed before agent_settled")
|
|
61
|
+
}
|
|
62
|
+
// The terminator travels behind an activity clear, so look for it rather than assuming it is first.
|
|
63
|
+
if len(ofType(m.Handle(pi.settled()), "chat/turnComplete")) == 0 {
|
|
64
|
+
t.Fatal("agent_settled did not close the turn")
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
twin.Run(t, "event-mapper", "reports an aborted run as cancelled, not complete", func(t *testing.T) {
|
|
69
|
+
r := runTurn(t, []obj{pi.agentStart(), pi.assistantStart(), pi.text(0, "partial"), pi.streamError("aborted by user", true), pi.settled()})
|
|
70
|
+
if got := str(r.state, "turns", 0, "state"); got != "cancelled" {
|
|
71
|
+
t.Fatalf("state = %q", got)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
twin.Run(t, "event-mapper", "reports a failed run as an error and marks the chat", func(t *testing.T) {
|
|
76
|
+
r := runTurn(t, []obj{pi.agentStart(), pi.assistantStart(), pi.streamError("overloaded_error", false), pi.settled()})
|
|
77
|
+
errs := ofType(r.actions, "chat/error")
|
|
78
|
+
if len(errs) != 1 {
|
|
79
|
+
t.Fatalf("chat/error actions = %d", len(errs))
|
|
80
|
+
}
|
|
81
|
+
equal(t, errs[0]["part"], obj{"kind": "error", "error": obj{"errorType": "agentRunFailed", "message": "overloaded_error"}}, "error part")
|
|
82
|
+
assertActionsValid(t, r.actions, "error action")
|
|
83
|
+
if got := str(r.state, "turns", 0, "state"); got != "error" {
|
|
84
|
+
t.Fatalf("state = %q", got)
|
|
85
|
+
}
|
|
86
|
+
if got := turnErrorMessage(r.state, 0); got != "overloaded_error" {
|
|
87
|
+
t.Fatalf("turn error = %q", got)
|
|
88
|
+
}
|
|
89
|
+
if !statusHas(r.state, float64(ahptypes.SessionStatusError)) {
|
|
90
|
+
t.Fatalf("chat status %v lacks the Error bit", r.state["status"])
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
twin.Run(t, "event-mapper", "ignores events after the turn has closed", func(t *testing.T) {
|
|
95
|
+
m := newMapper()
|
|
96
|
+
m.Handle(pi.settled())
|
|
97
|
+
if got := m.Handle(pi.text(0, "late")); len(got) != 0 {
|
|
98
|
+
t.Fatalf("late event produced %d actions", len(got))
|
|
99
|
+
}
|
|
100
|
+
if got := m.Finish("", ""); len(got) != 0 {
|
|
101
|
+
t.Fatalf("second finish produced %d actions", len(got))
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
twin.Run(t, "event-mapper", "can be closed without agent_settled", func(t *testing.T) {
|
|
106
|
+
// An extension command handled entirely by pi never reaches the model, so no agent event
|
|
107
|
+
// ever arrives; the turn must still terminate.
|
|
108
|
+
got := newMapper().Finish(mapper.OutcomeComplete, "")
|
|
109
|
+
if len(got) == 0 || actionType(got[0]) != "chat/turnComplete" {
|
|
110
|
+
t.Fatalf("finish(complete) = %v", got)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func TestEventMapperPartIdentity(t *testing.T) {
|
|
116
|
+
|
|
117
|
+
twin.Run(t, "event-mapper", "keeps contentIndex collisions across messages apart", func(t *testing.T) {
|
|
118
|
+
// pi restarts contentIndex at 0 for every assistant message; using it as a partId would
|
|
119
|
+
// append the second message's text to the first message's paragraph.
|
|
120
|
+
r := runTurn(t, []obj{
|
|
121
|
+
pi.agentStart(), pi.assistantStart(), pi.text(0, "first message"), pi.assistantEnd(nil),
|
|
122
|
+
pi.assistantStart(), pi.text(0, "second message"), pi.assistantEnd(nil), pi.settled(),
|
|
123
|
+
})
|
|
124
|
+
equal(t, contents(partsOfKind(r.state, "markdown")), []string{"first message", "second message"}, "markdown")
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
twin.Run(t, "event-mapper", "appends consecutive deltas into one part", func(t *testing.T) {
|
|
128
|
+
r := runTurn(t, []obj{pi.agentStart(), pi.assistantStart(), pi.text(0, "Hello"), pi.text(0, ", "), pi.text(0, "world"), pi.settled()})
|
|
129
|
+
if got := str(r.state, "turns", 0, "responseParts", 0, "content"); got != "Hello, world" {
|
|
130
|
+
t.Fatalf("content = %q", got)
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
twin.Run(t, "event-mapper", "routes thinking into a reasoning part, separate from markdown", func(t *testing.T) {
|
|
135
|
+
r := runTurn(t, []obj{pi.agentStart(), pi.assistantStart(), pi.thinking(0, "let me think"), pi.thinking(0, " harder"), pi.text(1, "the answer"), pi.settled()})
|
|
136
|
+
if k := str(r.state, "turns", 0, "responseParts", 0, "kind"); k != "reasoning" {
|
|
137
|
+
t.Fatalf("part 0 kind = %q", k)
|
|
138
|
+
}
|
|
139
|
+
if c := str(r.state, "turns", 0, "responseParts", 0, "content"); c != "let me think harder" {
|
|
140
|
+
t.Fatalf("reasoning = %q", c)
|
|
141
|
+
}
|
|
142
|
+
if k := str(r.state, "turns", 0, "responseParts", 1, "kind"); k != "markdown" {
|
|
143
|
+
t.Fatalf("part 1 kind = %q", k)
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
twin.Run(t, "event-mapper", "creates parts in arrival order so interleaving survives", func(t *testing.T) {
|
|
148
|
+
r := runTurn(t, []obj{
|
|
149
|
+
pi.agentStart(), pi.assistantStart(), pi.text(0, "before"), pi.toolStart(1, "tc-1", "bash"),
|
|
150
|
+
pi.toolEnd(1, "tc-1", "bash", obj{"command": "ls"}), pi.text(2, "after"), pi.settled(),
|
|
151
|
+
})
|
|
152
|
+
kinds := []string{}
|
|
153
|
+
for _, p := range list(r.state, "turns", 0, "responseParts") {
|
|
154
|
+
kinds = append(kinds, str(p, "kind"))
|
|
155
|
+
}
|
|
156
|
+
equal(t, kinds, []string{"markdown", "toolCall", "markdown"}, "kinds")
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
twin.Run(t, "event-mapper", "recovers a block a provider never streamed", func(t *testing.T) {
|
|
160
|
+
// Some models emit thinking_start / thinking_end with no thinking_delta in between.
|
|
161
|
+
r := runTurn(t, []obj{
|
|
162
|
+
pi.agentStart(), pi.assistantStart(), pi.thinkingEnd(0, "reasoned without streaming"),
|
|
163
|
+
pi.textEnd(1, "answered without streaming"), pi.settled(),
|
|
164
|
+
})
|
|
165
|
+
if k, c := str(r.state, "turns", 0, "responseParts", 0, "kind"), str(r.state, "turns", 0, "responseParts", 0, "content"); k != "reasoning" || c != "reasoned without streaming" {
|
|
166
|
+
t.Fatalf("part 0 = %s %q", k, c)
|
|
167
|
+
}
|
|
168
|
+
if k, c := str(r.state, "turns", 0, "responseParts", 1, "kind"), str(r.state, "turns", 0, "responseParts", 1, "content"); k != "markdown" || c != "answered without streaming" {
|
|
169
|
+
t.Fatalf("part 1 = %s %q", k, c)
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
twin.Run(t, "event-mapper", "does not duplicate a block that was streamed", func(t *testing.T) {
|
|
174
|
+
r := runTurn(t, []obj{pi.agentStart(), pi.assistantStart(), pi.text(0, "Hello"), pi.text(0, " world"), pi.textEnd(0, "Hello world"), pi.settled()})
|
|
175
|
+
parts := list(r.state, "turns", 0, "responseParts")
|
|
176
|
+
if len(parts) != 1 || str(parts[0], "content") != "Hello world" {
|
|
177
|
+
t.Fatalf("parts = %v", parts)
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
twin.Run(t, "event-mapper", "emits no part for an empty delta", func(t *testing.T) {
|
|
182
|
+
r := runTurn(t, []obj{pi.agentStart(), pi.assistantStart(), pi.text(0, ""), pi.settled()})
|
|
183
|
+
if n := len(list(r.state, "turns", 0, "responseParts")); n != 0 {
|
|
184
|
+
t.Fatalf("parts = %d", n)
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
func TestEventMapperToolCalls(t *testing.T) {
|
|
190
|
+
|
|
191
|
+
twin.Run(t, "event-mapper", "drives a tool call from streaming to completed without confirmation", func(t *testing.T) {
|
|
192
|
+
r := runTurn(t, []obj{
|
|
193
|
+
pi.agentStart(), pi.assistantStart(), pi.toolStart(0, "tc-1", "bash"), pi.toolDelta(0, `{"command":`), pi.toolDelta(0, `"ls"}`),
|
|
194
|
+
pi.toolEnd(0, "tc-1", "bash", obj{"command": "ls"}), pi.assistantEnd(nil), pi.execEnd("tc-1", "bash", "file-a\nfile-b", false), pi.settled(),
|
|
195
|
+
})
|
|
196
|
+
calls := toolCalls(r.state)
|
|
197
|
+
if len(calls) != 1 || str(calls[0], "status") != "completed" || str(calls[0], "toolName") != "bash" {
|
|
198
|
+
t.Fatalf("calls = %v", calls)
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
twin.Run(t, "event-mapper", "publishes partial output while a tool is still running", func(t *testing.T) {
|
|
203
|
+
r := runTurn(t, []obj{
|
|
204
|
+
pi.agentStart(), pi.assistantStart(), pi.toolStart(0, "tc-1", "bash"),
|
|
205
|
+
pi.toolEnd(0, "tc-1", "bash", obj{"command": "long-command"}), pi.execUpdate("tc-1", "bash", "partial output"),
|
|
206
|
+
})
|
|
207
|
+
calls := toolCalls(r.state)
|
|
208
|
+
if len(calls) != 1 || str(calls[0], "status") != "running" {
|
|
209
|
+
t.Fatalf("calls = %v", calls)
|
|
210
|
+
}
|
|
211
|
+
equal(t, calls[0]["content"], []any{obj{"type": "text", "text": "partial output"}}, "content")
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
twin.Run(t, "event-mapper", "never enters pending-confirmation", func(t *testing.T) {
|
|
215
|
+
// pi has no permission system, so there is nothing for a client to approve. Auto-confirming
|
|
216
|
+
// is the protocol's `confirmed` path and means no client renders approve/deny UI.
|
|
217
|
+
r := runTurn(t, []obj{
|
|
218
|
+
pi.agentStart(), pi.assistantStart(), pi.toolStart(0, "tc-1", "bash"),
|
|
219
|
+
pi.toolEnd(0, "tc-1", "bash", obj{"command": "ls"}), pi.execEnd("tc-1", "bash", "ok", false), pi.settled(),
|
|
220
|
+
})
|
|
221
|
+
ready := ofType(r.actions, "chat/toolCallReady")
|
|
222
|
+
if len(ready) != 1 || ready[0]["confirmed"] != "setting" {
|
|
223
|
+
t.Fatalf("ready = %v", ready)
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
twin.Run(t, "event-mapper", "marks a failed tool call as unsuccessful", func(t *testing.T) {
|
|
228
|
+
r := runTurn(t, []obj{
|
|
229
|
+
pi.agentStart(), pi.assistantStart(), pi.toolStart(0, "tc-1", "bash"),
|
|
230
|
+
pi.toolEnd(0, "tc-1", "bash", obj{"command": "false"}), pi.execEnd("tc-1", "bash", "boom", true), pi.settled(),
|
|
231
|
+
})
|
|
232
|
+
calls := toolCalls(r.state)
|
|
233
|
+
if len(calls) != 1 || str(calls[0], "status") != "completed" || calls[0]["success"] != false {
|
|
234
|
+
t.Fatalf("calls = %v", calls)
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
twin.Run(t, "event-mapper", "force-cancels a tool call still running when the turn ends", func(t *testing.T) {
|
|
239
|
+
// The reducer does this for us; asserting it pins the behaviour the mapper relies on when
|
|
240
|
+
// an abort lands mid-tool.
|
|
241
|
+
r := runTurn(t, []obj{
|
|
242
|
+
pi.agentStart(), pi.assistantStart(), pi.toolStart(0, "tc-1", "bash"),
|
|
243
|
+
pi.toolEnd(0, "tc-1", "bash", obj{"command": "sleep 100"}), pi.settled(),
|
|
244
|
+
})
|
|
245
|
+
calls := toolCalls(r.state)
|
|
246
|
+
if len(calls) != 1 || str(calls[0], "status") != "cancelled" {
|
|
247
|
+
t.Fatalf("calls = %v", calls)
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
twin.Run(t, "event-mapper", "ignores tool events for a call it never saw", func(t *testing.T) {
|
|
252
|
+
got := newMapper().Handle(pi.execEnd("unknown", "bash", "x", false))
|
|
253
|
+
// No tool-call action: there is nothing to complete. The activity change still goes out.
|
|
254
|
+
if n := len(ofType(got, "chat/toolCallComplete")); n != 0 {
|
|
255
|
+
t.Fatalf("completed %d unknown tool calls", n)
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
func TestEventMapperUsageAndSchema(t *testing.T) {
|
|
261
|
+
|
|
262
|
+
twin.Run(t, "event-mapper", "carries token usage onto the turn", func(t *testing.T) {
|
|
263
|
+
r := runTurn(t, []obj{
|
|
264
|
+
pi.agentStart(), pi.assistantStart(), pi.text(0, "hi"),
|
|
265
|
+
pi.assistantEnd(obj{"input": 100, "output": 20, "cacheRead": 5}), pi.settled(),
|
|
266
|
+
})
|
|
267
|
+
equal(t, get(r.state, "turns", 0, "usage"), obj{"inputTokens": 100, "outputTokens": 20, "cacheReadTokens": 5, "model": "test-model"}, "usage")
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
twin.Run(t, "event-mapper", "keeps the usage fields the protocol has no slot for", func(t *testing.T) {
|
|
271
|
+
// pi reports cache writes, a reasoning-token breakdown and computed cost; UsageInfo has
|
|
272
|
+
// fields for none of them. They ride in _meta.
|
|
273
|
+
r := runTurn(t, []obj{
|
|
274
|
+
pi.agentStart(), pi.assistantStart(), pi.text(0, "hi"),
|
|
275
|
+
pi.assistantEnd(obj{"input": 3, "output": 49, "cacheRead": 0, "cacheWrite": 2699, "reasoning": 20, "totalTokens": 2751}), pi.settled(),
|
|
276
|
+
})
|
|
277
|
+
equal(t, get(r.state, "turns", 0, "usage", "_meta"), obj{"cacheWriteTokens": 2699, "reasoningTokens": 20, "totalTokens": 2751}, "_meta")
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
twin.Run(t, "event-mapper", "emits only schema-conforming actions", func(t *testing.T) {
|
|
281
|
+
r := runTurn(t, []obj{
|
|
282
|
+
pi.agentStart(), pi.assistantStart(), pi.thinking(0, "hmm"), pi.text(1, "answer"),
|
|
283
|
+
pi.toolStart(2, "tc-1", "read"), pi.toolDelta(2, `{"path":"a"}`), pi.toolEnd(2, "tc-1", "read", obj{"path": "a"}),
|
|
284
|
+
pi.assistantEnd(obj{"input": 1, "output": 2, "cacheRead": 0}), pi.execEnd("tc-1", "read", "contents", false), pi.settled(),
|
|
285
|
+
})
|
|
286
|
+
assertActionsValid(t, r.actions, "turn")
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
func TestEventMapperOutcomeFromStopReason(t *testing.T) {
|
|
291
|
+
|
|
292
|
+
twin.Run(t, "event-mapper", "treats an aborted assistant message as a cancelled turn", func(t *testing.T) {
|
|
293
|
+
// Aborting before anything streams produces no `error` delta: the only signal is the
|
|
294
|
+
// finished message's stopReason (observed live in the `abort` fixture).
|
|
295
|
+
r := runTurn(t, []obj{
|
|
296
|
+
pi.agentStart(), pi.assistantStart(),
|
|
297
|
+
{"type": "message_end", "message": obj{"role": "assistant", "stopReason": "aborted", "errorMessage": "Request aborted"}},
|
|
298
|
+
pi.settled(),
|
|
299
|
+
})
|
|
300
|
+
if got := str(r.state, "turns", 0, "state"); got != "cancelled" {
|
|
301
|
+
t.Fatalf("state = %q", got)
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
twin.Run(t, "event-mapper", "treats a failed assistant message as an error turn", func(t *testing.T) {
|
|
306
|
+
r := runTurn(t, []obj{
|
|
307
|
+
pi.agentStart(), pi.assistantStart(),
|
|
308
|
+
{"type": "message_end", "message": obj{"role": "assistant", "stopReason": "error", "errorMessage": "upstream exploded"}},
|
|
309
|
+
pi.settled(),
|
|
310
|
+
})
|
|
311
|
+
if got := str(r.state, "turns", 0, "state"); got != "error" {
|
|
312
|
+
t.Fatalf("state = %q", got)
|
|
313
|
+
}
|
|
314
|
+
if got := turnErrorMessage(r.state, 0); got != "upstream exploded" {
|
|
315
|
+
t.Fatalf("error = %q", got)
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
func TestEventMapperInjectedMessages(t *testing.T) {
|
|
321
|
+
|
|
322
|
+
twin.Run(t, "event-mapper", "gives an injected user message its own turn", func(t *testing.T) {
|
|
323
|
+
// pi stores a steering message as an ordinary user message with nothing marking it as
|
|
324
|
+
// steering, so a rebuild from disk necessarily makes it a turn. The live path must match.
|
|
325
|
+
injected := []any{obj{"type": "text", "text": "Stop counting."}, obj{"type": "image", "data": onePixelPNG, "mimeType": "image/png"}}
|
|
326
|
+
r := runTurn(t, []obj{
|
|
327
|
+
pi.agentStart(), pi.assistantStart(), pi.text(0, "1\n2\n3"), pi.assistantEnd(nil),
|
|
328
|
+
{"type": "message_start", "message": obj{"role": "user", "content": injected}},
|
|
329
|
+
{"type": "message_end", "message": obj{"role": "user", "content": injected}},
|
|
330
|
+
pi.assistantStart(), pi.text(0, "STOPPED"), pi.assistantEnd(nil), pi.settled(),
|
|
331
|
+
})
|
|
332
|
+
if n := len(list(r.state, "turns")); n != 2 {
|
|
333
|
+
t.Fatalf("turns = %d", n)
|
|
334
|
+
}
|
|
335
|
+
if got := str(r.state, "turns", 0, "message", "text"); got != "Hello" {
|
|
336
|
+
t.Fatalf("turn 0 text = %q", got)
|
|
337
|
+
}
|
|
338
|
+
if got := str(r.state, "turns", 1, "message", "text"); got != "Stop counting." {
|
|
339
|
+
t.Fatalf("turn 1 text = %q", got)
|
|
340
|
+
}
|
|
341
|
+
equal(t, get(r.state, "turns", 1, "message", "attachments"), []any{obj{
|
|
342
|
+
"type": "embeddedResource", "label": "Image 1", "displayKind": "image", "data": onePixelPNG, "contentType": "image/png",
|
|
343
|
+
}}, "attachments")
|
|
344
|
+
if got := str(r.state, "turns", 1, "responseParts", 0, "content"); got != "STOPPED" {
|
|
345
|
+
t.Fatalf("turn 1 response = %q", got)
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
twin.Run(t, "event-mapper", "does not split on the run's own opening prompt", func(t *testing.T) {
|
|
350
|
+
// The first user message is the prompt the client already opened the turn with; splitting
|
|
351
|
+
// there would produce an empty leading turn.
|
|
352
|
+
r := runTurn(t, []obj{
|
|
353
|
+
pi.agentStart(),
|
|
354
|
+
{"type": "message_start", "message": obj{"role": "user", "content": "Hello"}},
|
|
355
|
+
{"type": "message_end", "message": obj{"role": "user", "content": "Hello"}},
|
|
356
|
+
pi.assistantStart(), pi.text(0, "hi"), pi.settled(),
|
|
357
|
+
})
|
|
358
|
+
if n := len(list(r.state, "turns")); n != 1 {
|
|
359
|
+
t.Fatalf("turns = %d", n)
|
|
360
|
+
}
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
twin.Run(t, "event-mapper", "keeps part ids unique across a split", func(t *testing.T) {
|
|
364
|
+
r := runTurn(t, []obj{
|
|
365
|
+
pi.agentStart(), pi.assistantStart(), pi.text(0, "before"),
|
|
366
|
+
{"type": "message_start", "message": obj{"role": "user", "content": "switch"}},
|
|
367
|
+
pi.assistantStart(), pi.text(0, "after"), pi.settled(),
|
|
368
|
+
})
|
|
369
|
+
seen := map[string]bool{}
|
|
370
|
+
for _, a := range ofType(r.actions, "chat/responsePart") {
|
|
371
|
+
id := str(a, "part", "id")
|
|
372
|
+
if seen[id] {
|
|
373
|
+
t.Fatalf("part id %q collided across the split", id)
|
|
374
|
+
}
|
|
375
|
+
seen[id] = true
|
|
376
|
+
}
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
var _ = base64.StdEncoding
|