@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,600 @@
|
|
|
1
|
+
package mapper
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"strconv"
|
|
6
|
+
"strings"
|
|
7
|
+
"time"
|
|
8
|
+
|
|
9
|
+
"github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// Event is one pi agent event, decoded JSON.
|
|
13
|
+
type Event = map[string]any
|
|
14
|
+
|
|
15
|
+
// Outcome is how a turn ended.
|
|
16
|
+
type Outcome string
|
|
17
|
+
|
|
18
|
+
const (
|
|
19
|
+
// OutcomeDefault lets the mapper decide from what it saw.
|
|
20
|
+
OutcomeDefault Outcome = ""
|
|
21
|
+
OutcomeComplete Outcome = "complete"
|
|
22
|
+
OutcomeCancelled Outcome = "cancelled"
|
|
23
|
+
OutcomeError Outcome = "error"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
// Options configure a TurnMapper.
|
|
27
|
+
type Options struct {
|
|
28
|
+
// WorkingDirectory renders tool paths relative to it, so activity reads as `Reading src/x.ts`.
|
|
29
|
+
WorkingDirectory string
|
|
30
|
+
// Now is the clock (defaults to time.Now); tests pin it.
|
|
31
|
+
Now func() time.Time
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type openPart struct {
|
|
35
|
+
partID string
|
|
36
|
+
kind ahptypes.ResponsePartKind
|
|
37
|
+
created bool
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// TurnMapper maps one AHP turn (port of the TurnMapper class of src/pi/event-mapper.ts).
|
|
41
|
+
//
|
|
42
|
+
// Three structural mismatches it bridges:
|
|
43
|
+
//
|
|
44
|
+
// 1. Turn granularity. pi nests an agent run (agent_start..agent_end), a pi turn (one assistant
|
|
45
|
+
// message) and content blocks; AHP has a turn (one user message through to completion) and
|
|
46
|
+
// its response parts. One AHP turn therefore spans everything from prompt() to agent_settled,
|
|
47
|
+
// which may hold several agent runs (auto-retry, post-compaction retry). Mapping agent_end to
|
|
48
|
+
// chat/turnComplete would close the turn early and the reducer would drop every later action.
|
|
49
|
+
// 2. Part identity. contentIndex restarts at 0 for every assistant message and the stream has no
|
|
50
|
+
// message id, so parts are keyed `<turnId>:<message ordinal>:<contentIndex>`.
|
|
51
|
+
// 3. Part creation. chat/delta appends to a part that must already exist (an unknown partId is a
|
|
52
|
+
// silent no-op), so parts are created lazily, on the first content that lands in them.
|
|
53
|
+
//
|
|
54
|
+
// A mapper is created when a turn starts and discarded when it ends. It is not safe for
|
|
55
|
+
// concurrent use; the caller serialises Handle and Finish.
|
|
56
|
+
type TurnMapper struct {
|
|
57
|
+
turnID string // the turn being mapped; changes when a message is injected mid-run
|
|
58
|
+
rootTurnID string
|
|
59
|
+
turnStartedAt int64 // ms since the epoch
|
|
60
|
+
injectedCount int
|
|
61
|
+
|
|
62
|
+
messageOrdinal int
|
|
63
|
+
parts map[int]*openPart
|
|
64
|
+
toolCallsByIdx map[int]string
|
|
65
|
+
liveToolCalls map[string]string // toolCallId -> how the call reads once it has finished
|
|
66
|
+
outcome Outcome
|
|
67
|
+
errorMessage *string
|
|
68
|
+
finished bool
|
|
69
|
+
|
|
70
|
+
opts Options
|
|
71
|
+
activity *string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// NewTurnMapper creates a mapper for a turn that started at startedAtMs (ms since the epoch).
|
|
75
|
+
func NewTurnMapper(turnID string, startedAtMs int64, opts ...Options) *TurnMapper {
|
|
76
|
+
m := &TurnMapper{
|
|
77
|
+
turnID: turnID, rootTurnID: turnID, turnStartedAt: startedAtMs,
|
|
78
|
+
messageOrdinal: -1, parts: map[int]*openPart{}, toolCallsByIdx: map[int]string{},
|
|
79
|
+
liveToolCalls: map[string]string{}, outcome: OutcomeComplete,
|
|
80
|
+
}
|
|
81
|
+
if len(opts) > 0 {
|
|
82
|
+
m.opts = opts[0]
|
|
83
|
+
}
|
|
84
|
+
if m.opts.Now == nil {
|
|
85
|
+
m.opts.Now = time.Now
|
|
86
|
+
}
|
|
87
|
+
return m
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// TurnID is the turn currently being mapped.
|
|
91
|
+
func (m *TurnMapper) TurnID() string { return m.turnID }
|
|
92
|
+
|
|
93
|
+
// Finished reports whether the turn has been closed.
|
|
94
|
+
func (m *TurnMapper) Finished() bool { return m.finished }
|
|
95
|
+
|
|
96
|
+
func (m *TurnMapper) nowMs() int64 { return m.opts.Now().UnixMilli() }
|
|
97
|
+
|
|
98
|
+
// Handle translates one pi event into zero or more chat actions.
|
|
99
|
+
func (m *TurnMapper) Handle(event Event) []ahptypes.StateAction {
|
|
100
|
+
if m.finished {
|
|
101
|
+
return nil
|
|
102
|
+
}
|
|
103
|
+
typ, _ := event["type"].(string)
|
|
104
|
+
switch typ {
|
|
105
|
+
case "message_start":
|
|
106
|
+
return m.onMessageStart(event)
|
|
107
|
+
case "message_update":
|
|
108
|
+
return m.onDelta(asMap(event["assistantMessageEvent"]))
|
|
109
|
+
case "message_end":
|
|
110
|
+
return m.onMessageEnd(event)
|
|
111
|
+
case "tool_execution_start":
|
|
112
|
+
// The tool is about to run: say which one, and with what.
|
|
113
|
+
name, _ := event["toolName"].(string)
|
|
114
|
+
return m.setActivity(strPtr(DescribeToolCall(name, event["args"], m.opts.WorkingDirectory)))
|
|
115
|
+
case "tool_execution_update":
|
|
116
|
+
return m.onToolUpdate(event)
|
|
117
|
+
case "tool_execution_end":
|
|
118
|
+
out := m.onToolEnd(event)
|
|
119
|
+
// Back to the model. Kept out of onToolEnd, which bails early for a call it never saw
|
|
120
|
+
// start: activity is display state and should not depend on that bookkeeping.
|
|
121
|
+
return append(out, m.setActivity(strPtr(ThinkingActivity))...)
|
|
122
|
+
case "agent_settled":
|
|
123
|
+
return m.Finish(OutcomeDefault, "")
|
|
124
|
+
case "agent_start":
|
|
125
|
+
// A run beginning, or resuming after a retry.
|
|
126
|
+
return m.setActivity(strPtr(ThinkingActivity))
|
|
127
|
+
default:
|
|
128
|
+
// agent_end / turn_start / turn_end carry no protocol-visible state of their own: a retry
|
|
129
|
+
// re-enters agent_start inside the same AHP turn, and pi's turn boundaries are just
|
|
130
|
+
// groupings of response parts.
|
|
131
|
+
return nil
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
func asMap(v any) map[string]any { m, _ := v.(map[string]any); return m }
|
|
136
|
+
|
|
137
|
+
// ── Assistant messages ──────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
func (m *TurnMapper) onMessageStart(event Event) []ahptypes.StateAction {
|
|
140
|
+
message := asMap(event["message"])
|
|
141
|
+
switch message["role"] {
|
|
142
|
+
case "user":
|
|
143
|
+
return m.onInjectedMessage(message)
|
|
144
|
+
case "assistant":
|
|
145
|
+
// A new assistant message restarts contentIndex at 0, so the part map must not leak.
|
|
146
|
+
m.messageOrdinal++
|
|
147
|
+
m.parts = map[int]*openPart{}
|
|
148
|
+
m.toolCallsByIdx = map[int]string{}
|
|
149
|
+
}
|
|
150
|
+
return nil
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// setActivity emits an activity change, skipping no-ops: a turn fires many events that map to
|
|
154
|
+
// the same description, and re-sending it would burn a serverSeq per token.
|
|
155
|
+
func (m *TurnMapper) setActivity(activity *string) []ahptypes.StateAction {
|
|
156
|
+
if (m.activity == nil && activity == nil) || (m.activity != nil && activity != nil && *m.activity == *activity) {
|
|
157
|
+
return nil
|
|
158
|
+
}
|
|
159
|
+
m.activity = activity
|
|
160
|
+
return []ahptypes.StateAction{ahptypes.StateAction{Value: &ahptypes.ChatActivityChangedAction{Type: ahptypes.ActionTypeChatActivityChanged, Activity: activity}}}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
func (m *TurnMapper) onMessageEnd(event Event) []ahptypes.StateAction {
|
|
164
|
+
message := asMap(event["message"])
|
|
165
|
+
if message["role"] != "assistant" {
|
|
166
|
+
return nil
|
|
167
|
+
}
|
|
168
|
+
// A run cancelled or failed before any content streamed produces no `error` delta: the outcome
|
|
169
|
+
// shows up only as the finished message's stopReason. Reading it here keeps such a turn from
|
|
170
|
+
// being reported as a normal completion.
|
|
171
|
+
switch message["stopReason"] {
|
|
172
|
+
case "aborted":
|
|
173
|
+
m.outcome = OutcomeCancelled
|
|
174
|
+
m.errorMessageIfUnset(message["errorMessage"])
|
|
175
|
+
case "error":
|
|
176
|
+
m.outcome = OutcomeError
|
|
177
|
+
m.errorMessageIfUnset(message["errorMessage"])
|
|
178
|
+
}
|
|
179
|
+
model, _ := message["model"].(string)
|
|
180
|
+
if usage := toUsageInfo(asMap(message["usage"]), model); usage != nil {
|
|
181
|
+
return []ahptypes.StateAction{ahptypes.StateAction{Value: &ahptypes.ChatUsageAction{Type: ahptypes.ActionTypeChatUsage, TurnId: m.turnID, Usage: *usage}}}
|
|
182
|
+
}
|
|
183
|
+
return nil
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// errorMessageIfUnset is `this.#errorMessage ??= value`.
|
|
187
|
+
func (m *TurnMapper) errorMessageIfUnset(v any) {
|
|
188
|
+
if m.errorMessage == nil {
|
|
189
|
+
if s, ok := v.(string); ok {
|
|
190
|
+
m.errorMessage = &s
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
func toUsageInfo(usage map[string]any, model string) *ahptypes.UsageInfo {
|
|
196
|
+
if usage == nil {
|
|
197
|
+
return nil
|
|
198
|
+
}
|
|
199
|
+
// UsageInfo only has slots for input/output/cacheRead, but pi reports more (cache writes, a
|
|
200
|
+
// reasoning-token breakdown, computed cost). Dropping them would lose the numbers a client
|
|
201
|
+
// needs to show what a turn cost, so the remainder rides in `_meta`, the protocol's place for
|
|
202
|
+
// provider-specific extras.
|
|
203
|
+
extra := map[string]json.RawMessage{}
|
|
204
|
+
put := func(key string, v any) {
|
|
205
|
+
if raw, err := json.Marshal(v); err == nil {
|
|
206
|
+
extra[key] = raw
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if v, ok := usage["cacheWrite"]; ok && v != nil {
|
|
210
|
+
put("cacheWriteTokens", v)
|
|
211
|
+
}
|
|
212
|
+
if v, ok := usage["reasoning"]; ok && v != nil {
|
|
213
|
+
put("reasoningTokens", v)
|
|
214
|
+
}
|
|
215
|
+
if v, ok := usage["totalTokens"]; ok && v != nil {
|
|
216
|
+
put("totalTokens", v)
|
|
217
|
+
}
|
|
218
|
+
if v, ok := usage["cost"]; ok && v != nil {
|
|
219
|
+
put("cost", v)
|
|
220
|
+
}
|
|
221
|
+
info := &ahptypes.UsageInfo{}
|
|
222
|
+
if f, ok := num(usage["input"]); ok {
|
|
223
|
+
info.InputTokens = i64Ptr(int64(f))
|
|
224
|
+
}
|
|
225
|
+
if f, ok := num(usage["output"]); ok {
|
|
226
|
+
info.OutputTokens = i64Ptr(int64(f))
|
|
227
|
+
}
|
|
228
|
+
if f, ok := num(usage["cacheRead"]); ok {
|
|
229
|
+
info.CacheReadTokens = i64Ptr(int64(f))
|
|
230
|
+
}
|
|
231
|
+
if model != "" {
|
|
232
|
+
info.Model = &model
|
|
233
|
+
}
|
|
234
|
+
if len(extra) > 0 {
|
|
235
|
+
info.Meta = extra
|
|
236
|
+
}
|
|
237
|
+
return info
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// onInjectedMessage handles a user message pi injects part-way through a run.
|
|
241
|
+
//
|
|
242
|
+
// pi delivers a steering message as an ordinary user message inside the running turn and stores
|
|
243
|
+
// it in the session file as an ordinary user message too, with nothing to mark it as steering. So
|
|
244
|
+
// when the conversation is rebuilt from disk it necessarily becomes its own turn, because a user
|
|
245
|
+
// message is what starts one. The live path does the same, or the conversation would render one
|
|
246
|
+
// way while running and another after a reload: the turn in flight is closed and a fresh one
|
|
247
|
+
// opened around the injected message.
|
|
248
|
+
//
|
|
249
|
+
// The first user message of a run is the prompt itself, which the client already opened the turn
|
|
250
|
+
// with, so only later ones split.
|
|
251
|
+
func (m *TurnMapper) onInjectedMessage(message map[string]any) []ahptypes.StateAction {
|
|
252
|
+
if m.messageOrdinal < 0 {
|
|
253
|
+
return nil // nothing has streamed yet: this is the run's own prompt
|
|
254
|
+
}
|
|
255
|
+
injected := UserMessageFromPiContent(message["content"])
|
|
256
|
+
completed := m.closeCurrentTurn()
|
|
257
|
+
|
|
258
|
+
m.injectedCount++
|
|
259
|
+
m.turnID = m.rootTurnID + "#" + strconv.Itoa(m.injectedCount)
|
|
260
|
+
m.turnStartedAt = m.nowMs()
|
|
261
|
+
m.messageOrdinal = -1
|
|
262
|
+
m.parts = map[int]*openPart{}
|
|
263
|
+
m.toolCallsByIdx = map[int]string{}
|
|
264
|
+
|
|
265
|
+
return append(completed, ahptypes.StateAction{Value: &ahptypes.ChatTurnStartedAction{
|
|
266
|
+
Type: ahptypes.ActionTypeChatTurnStarted, TurnId: m.turnID,
|
|
267
|
+
StartedAt: isoTime(m.turnStartedAt), Message: injected,
|
|
268
|
+
}})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
func (m *TurnMapper) closeCurrentTurn() []ahptypes.StateAction {
|
|
272
|
+
return []ahptypes.StateAction{ahptypes.StateAction{Value: &ahptypes.ChatTurnCompleteAction{
|
|
273
|
+
Type: ahptypes.ActionTypeChatTurnComplete, TurnId: m.turnID, Duration: m.duration(),
|
|
274
|
+
}}}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
func (m *TurnMapper) duration() int64 {
|
|
278
|
+
if d := m.nowMs() - m.turnStartedAt; d > 0 {
|
|
279
|
+
return d
|
|
280
|
+
}
|
|
281
|
+
return 0
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// isoTime is Date.toISOString.
|
|
285
|
+
func isoTime(ms int64) string { return time.UnixMilli(ms).UTC().Format("2006-01-02T15:04:05.000Z") }
|
|
286
|
+
|
|
287
|
+
// ── Streaming content ───────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
func (m *TurnMapper) onDelta(delta map[string]any) []ahptypes.StateAction {
|
|
290
|
+
typ, _ := delta["type"].(string)
|
|
291
|
+
switch typ {
|
|
292
|
+
case "text_delta":
|
|
293
|
+
return m.appendText(delta, ahptypes.ResponsePartKindMarkdown, false)
|
|
294
|
+
case "thinking_delta":
|
|
295
|
+
return m.appendText(delta, ahptypes.ResponsePartKindReasoning, false)
|
|
296
|
+
case "text_end":
|
|
297
|
+
return m.closeBlock(delta, ahptypes.ResponsePartKindMarkdown)
|
|
298
|
+
case "thinking_end":
|
|
299
|
+
return m.closeBlock(delta, ahptypes.ResponsePartKindReasoning)
|
|
300
|
+
case "toolcall_start":
|
|
301
|
+
return m.onToolCallStart(delta)
|
|
302
|
+
case "toolcall_delta":
|
|
303
|
+
return m.onToolCallDelta(delta)
|
|
304
|
+
case "toolcall_end":
|
|
305
|
+
return m.onToolCallReady(delta)
|
|
306
|
+
case "error":
|
|
307
|
+
// Recorded, not emitted: a mid-stream error may still be retried inside the same AHP turn,
|
|
308
|
+
// so only agent_settled decides the turn's outcome.
|
|
309
|
+
if dig(delta, "error", "stopReason") == "aborted" {
|
|
310
|
+
m.outcome = OutcomeCancelled
|
|
311
|
+
} else {
|
|
312
|
+
m.outcome = OutcomeError
|
|
313
|
+
}
|
|
314
|
+
m.errorMessage = nil
|
|
315
|
+
if s, ok := digStr(delta, "error", "errorMessage"); ok {
|
|
316
|
+
m.errorMessage = &s
|
|
317
|
+
}
|
|
318
|
+
return nil
|
|
319
|
+
default:
|
|
320
|
+
// `*_start` / `start` / `done` need no action: parts are created lazily by the first delta.
|
|
321
|
+
return nil
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// closeBlock handles the end of a text or reasoning block. Normally a no-op, because the deltas
|
|
326
|
+
// already built the part. A provider that does not stream a block incrementally emits only
|
|
327
|
+
// `*_end` with the finished content; without this the content would be silently dropped.
|
|
328
|
+
func (m *TurnMapper) closeBlock(delta map[string]any, kind ahptypes.ResponsePartKind) []ahptypes.StateAction {
|
|
329
|
+
idx := intOr(delta["contentIndex"], 0)
|
|
330
|
+
if p := m.parts[idx]; p != nil && p.created {
|
|
331
|
+
return nil
|
|
332
|
+
}
|
|
333
|
+
content, _ := delta["content"].(string)
|
|
334
|
+
return m.appendTextValue(idx, content, kind)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
func (m *TurnMapper) partID(idx int) string {
|
|
338
|
+
return m.turnID + ":" + strconv.Itoa(m.messageOrdinal) + ":" + strconv.Itoa(idx)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
func (m *TurnMapper) appendText(delta map[string]any, kind ahptypes.ResponsePartKind, _ bool) []ahptypes.StateAction {
|
|
342
|
+
text, _ := delta["delta"].(string)
|
|
343
|
+
return m.appendTextValue(intOr(delta["contentIndex"], 0), text, kind)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// appendTextValue appends streamed text, creating the part on first use. The first chunk rides
|
|
347
|
+
// along on chat/responsePart rather than a separate chat/delta, which halves the actions for
|
|
348
|
+
// short responses without changing the reduced state.
|
|
349
|
+
func (m *TurnMapper) appendTextValue(idx int, text string, kind ahptypes.ResponsePartKind) []ahptypes.StateAction {
|
|
350
|
+
if len(text) == 0 {
|
|
351
|
+
return nil
|
|
352
|
+
}
|
|
353
|
+
out := m.setActivity(strPtr(RespondingActivity))
|
|
354
|
+
part := m.parts[idx]
|
|
355
|
+
if part == nil {
|
|
356
|
+
part = &openPart{partID: m.partID(idx), kind: kind}
|
|
357
|
+
m.parts[idx] = part
|
|
358
|
+
}
|
|
359
|
+
if !part.created {
|
|
360
|
+
part.created = true
|
|
361
|
+
var rp ahptypes.ResponsePart
|
|
362
|
+
if kind == ahptypes.ResponsePartKindMarkdown {
|
|
363
|
+
rp = ahptypes.ResponsePart{Value: &ahptypes.MarkdownResponsePart{Kind: kind, Id: part.partID, Content: text}}
|
|
364
|
+
} else {
|
|
365
|
+
rp = ahptypes.ResponsePart{Value: &ahptypes.ReasoningResponsePart{Kind: kind, Id: part.partID, Content: text}}
|
|
366
|
+
}
|
|
367
|
+
return append(out, ahptypes.StateAction{Value: &ahptypes.ChatResponsePartAction{Type: ahptypes.ActionTypeChatResponsePart, TurnId: m.turnID, Part: rp}})
|
|
368
|
+
}
|
|
369
|
+
if kind == ahptypes.ResponsePartKindMarkdown {
|
|
370
|
+
return append(out, ahptypes.StateAction{Value: &ahptypes.ChatDeltaAction{Type: ahptypes.ActionTypeChatDelta, TurnId: m.turnID, PartId: part.partID, Content: text}})
|
|
371
|
+
}
|
|
372
|
+
return append(out, ahptypes.StateAction{Value: &ahptypes.ChatReasoningAction{Type: ahptypes.ActionTypeChatReasoning, TurnId: m.turnID, PartId: part.partID, Content: text}})
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ── Tool calls ──────────────────────────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
func (m *TurnMapper) onToolCallStart(delta map[string]any) []ahptypes.StateAction {
|
|
378
|
+
idx := intOr(delta["contentIndex"], 0)
|
|
379
|
+
block := asMap(dig(delta, "partial", "content", idx))
|
|
380
|
+
id, _ := block["id"].(string)
|
|
381
|
+
if id == "" {
|
|
382
|
+
// Without an id there is nothing to correlate later; wait for the next delta, which
|
|
383
|
+
// carries the resolved block.
|
|
384
|
+
return nil
|
|
385
|
+
}
|
|
386
|
+
m.toolCallsByIdx[idx] = id
|
|
387
|
+
name := "tool"
|
|
388
|
+
if s, ok := block["name"].(string); ok {
|
|
389
|
+
name = s
|
|
390
|
+
}
|
|
391
|
+
return []ahptypes.StateAction{ahptypes.StateAction{Value: &ahptypes.ChatToolCallStartAction{
|
|
392
|
+
Type: ahptypes.ActionTypeChatToolCallStart, TurnId: m.turnID, ToolCallId: id, ToolName: name, DisplayName: name,
|
|
393
|
+
}}}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
func (m *TurnMapper) onToolCallDelta(delta map[string]any) []ahptypes.StateAction {
|
|
397
|
+
idx := intOr(delta["contentIndex"], 0)
|
|
398
|
+
content, _ := delta["delta"].(string)
|
|
399
|
+
deltaAction := func(id string) ahptypes.StateAction {
|
|
400
|
+
return ahptypes.StateAction{Value: &ahptypes.ChatToolCallDeltaAction{Type: ahptypes.ActionTypeChatToolCallDelta, TurnId: m.turnID, ToolCallId: id, Content: &content}}
|
|
401
|
+
}
|
|
402
|
+
if id, ok := m.toolCallsByIdx[idx]; ok && id != "" {
|
|
403
|
+
return []ahptypes.StateAction{deltaAction(id)}
|
|
404
|
+
}
|
|
405
|
+
// The id may only have materialised after toolcall_start; recover from the partial message
|
|
406
|
+
// rather than dropping the call.
|
|
407
|
+
started := m.onToolCallStart(delta)
|
|
408
|
+
id := m.toolCallsByIdx[idx]
|
|
409
|
+
if id == "" {
|
|
410
|
+
return nil
|
|
411
|
+
}
|
|
412
|
+
return append(started, deltaAction(id))
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// onToolCallReady: parameters are complete. `confirmed: 'setting'` sends the call straight to
|
|
416
|
+
// running, skipping pending-confirmation. That is a first-class path in the protocol and the
|
|
417
|
+
// honest one here: pi has no permission system, so there is nothing for a client to approve.
|
|
418
|
+
// Because the call never enters pending-confirmation a conforming client renders no approve/deny
|
|
419
|
+
// UI; cancelling the whole turn stays available and maps to abort().
|
|
420
|
+
func (m *TurnMapper) onToolCallReady(delta map[string]any) []ahptypes.StateAction {
|
|
421
|
+
idx := intOr(delta["contentIndex"], 0)
|
|
422
|
+
toolCall := asMap(delta["toolCall"])
|
|
423
|
+
id, hasID := toolCall["id"].(string)
|
|
424
|
+
if !hasID {
|
|
425
|
+
id = m.toolCallsByIdx[idx]
|
|
426
|
+
}
|
|
427
|
+
if id == "" {
|
|
428
|
+
return nil
|
|
429
|
+
}
|
|
430
|
+
m.toolCallsByIdx[idx] = id
|
|
431
|
+
name := "tool"
|
|
432
|
+
if s, ok := toolCall["name"].(string); ok {
|
|
433
|
+
name = s
|
|
434
|
+
}
|
|
435
|
+
args := toolCall["arguments"]
|
|
436
|
+
cwd := m.opts.WorkingDirectory
|
|
437
|
+
description := DescribeToolCall(name, args, cwd)
|
|
438
|
+
m.liveToolCalls[id] = DescribeFinishedToolCall(name, args, cwd)
|
|
439
|
+
confirmed := ahptypes.ToolCallConfirmationReasonSetting
|
|
440
|
+
ready := &ahptypes.ChatToolCallReadyAction{
|
|
441
|
+
Type: ahptypes.ActionTypeChatToolCallReady, TurnId: m.turnID, ToolCallId: id,
|
|
442
|
+
// Rendered while the call runs, and the same phrasing the activity indicator uses, so the
|
|
443
|
+
// two do not describe one call differently.
|
|
444
|
+
InvocationMessage: ahptypes.NewStringOrMarkdownPlain(description),
|
|
445
|
+
Confirmed: &confirmed,
|
|
446
|
+
}
|
|
447
|
+
if input, ok := ToolInputFor(name, args); ok {
|
|
448
|
+
ready.ToolInput = &ahptypes.ToolInput{Inline: &input}
|
|
449
|
+
}
|
|
450
|
+
return []ahptypes.StateAction{ahptypes.StateAction{Value: ready}}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
func (m *TurnMapper) onToolUpdate(event Event) []ahptypes.StateAction {
|
|
454
|
+
id, _ := event["toolCallId"].(string)
|
|
455
|
+
if _, live := m.liveToolCalls[id]; !live {
|
|
456
|
+
return nil
|
|
457
|
+
}
|
|
458
|
+
content := toolResultContent(event["partialResult"])
|
|
459
|
+
if content == nil {
|
|
460
|
+
return nil
|
|
461
|
+
}
|
|
462
|
+
return []ahptypes.StateAction{ahptypes.StateAction{Value: &ahptypes.ChatToolCallContentChangedAction{
|
|
463
|
+
Type: ahptypes.ActionTypeChatToolCallContentChanged, TurnId: m.turnID, ToolCallId: id, Content: content,
|
|
464
|
+
}}}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
func textContent(text string) []ahptypes.ToolResultContent {
|
|
468
|
+
return []ahptypes.ToolResultContent{{Value: &ahptypes.ToolResultTextContent{Type: ahptypes.ToolResultContentTypeText, Text: text}}}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// toolResultContent flattens pi's tool result content blocks into the protocol's text blocks.
|
|
472
|
+
func toolResultContent(result any) []ahptypes.ToolResultContent {
|
|
473
|
+
blocks, ok := dig(result, "content").([]any)
|
|
474
|
+
if !ok {
|
|
475
|
+
return nil
|
|
476
|
+
}
|
|
477
|
+
var texts []string
|
|
478
|
+
for _, b := range blocks {
|
|
479
|
+
if bm := asMap(b); bm["type"] == "text" {
|
|
480
|
+
if s, ok := bm["text"].(string); ok && s != "" {
|
|
481
|
+
texts = append(texts, s)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if len(texts) == 0 {
|
|
486
|
+
return nil
|
|
487
|
+
}
|
|
488
|
+
return textContent(strings.Join(texts, "\n"))
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// editPatch is the unified diff pi computes for an edit, if the result carries one. `edit` is the
|
|
492
|
+
// only built-in tool that reports structured details; its text says how many blocks were replaced
|
|
493
|
+
// (a count a client cannot render or check), and the patch beside it is the part worth showing.
|
|
494
|
+
func editPatch(result any) (string, bool) {
|
|
495
|
+
s, ok := digStr(result, "details", "patch")
|
|
496
|
+
return s, ok && s != ""
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// textOf is the text a tool reported, as a client would render it.
|
|
500
|
+
func textOf(content []ahptypes.ToolResultContent) string {
|
|
501
|
+
var sb strings.Builder
|
|
502
|
+
for _, c := range content {
|
|
503
|
+
if t, ok := c.Value.(*ahptypes.ToolResultTextContent); ok {
|
|
504
|
+
sb.WriteString(t.Text)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return trimJS(sb.String())
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
func (m *TurnMapper) onToolEnd(event Event) []ahptypes.StateAction {
|
|
511
|
+
id, _ := event["toolCallId"].(string)
|
|
512
|
+
finished, live := m.liveToolCalls[id]
|
|
513
|
+
if !live {
|
|
514
|
+
return nil
|
|
515
|
+
}
|
|
516
|
+
delete(m.liveToolCalls, id)
|
|
517
|
+
toolName, _ := event["toolName"].(string)
|
|
518
|
+
success := event["isError"] != true
|
|
519
|
+
var content []ahptypes.ToolResultContent
|
|
520
|
+
if patch, ok := editPatch(event["result"]); ok {
|
|
521
|
+
content = textContent(patch)
|
|
522
|
+
} else {
|
|
523
|
+
content = toolResultContent(event["result"])
|
|
524
|
+
}
|
|
525
|
+
// A failed tool has already said why: an ENOENT naming the path it could not open, a non-zero
|
|
526
|
+
// exit with its stderr. That text belongs in error.message, where a client looks.
|
|
527
|
+
reported := textOf(content)
|
|
528
|
+
var past string
|
|
529
|
+
switch {
|
|
530
|
+
case success && live && finished != "":
|
|
531
|
+
past = finished
|
|
532
|
+
case success:
|
|
533
|
+
past = "Ran " + toolName
|
|
534
|
+
case finished != "":
|
|
535
|
+
past = finished + " failed"
|
|
536
|
+
default:
|
|
537
|
+
past = toolName + " failed"
|
|
538
|
+
}
|
|
539
|
+
result := ahptypes.ToolCallResult{Success: success, PastTenseMessage: ahptypes.NewStringOrMarkdownPlain(past), Content: content}
|
|
540
|
+
if !success {
|
|
541
|
+
message := reported
|
|
542
|
+
if message == "" {
|
|
543
|
+
message = toolName + " failed"
|
|
544
|
+
}
|
|
545
|
+
// ToolCallResult.error is its own shape (message + code), not ErrorInfo.
|
|
546
|
+
raw, _ := json.Marshal(map[string]string{"message": message})
|
|
547
|
+
rm := json.RawMessage(raw)
|
|
548
|
+
result.Error = &rm
|
|
549
|
+
}
|
|
550
|
+
return []ahptypes.StateAction{ahptypes.StateAction{Value: &ahptypes.ChatToolCallCompleteAction{
|
|
551
|
+
Type: ahptypes.ActionTypeChatToolCallComplete, TurnId: m.turnID, ToolCallId: id, Result: result,
|
|
552
|
+
}}}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ── Turn termination ────────────────────────────────────────────────────
|
|
556
|
+
|
|
557
|
+
// Finish closes the turn. It is also called directly when a turn ends without agent_settled: an
|
|
558
|
+
// extension command that never reaches the model, or a failed prompt(). Without it the turn would
|
|
559
|
+
// stay active forever and the session would sit at InProgress. outcome "" lets the mapper decide;
|
|
560
|
+
// message "" means no explicit error message.
|
|
561
|
+
func (m *TurnMapper) Finish(outcome Outcome, message string) []ahptypes.StateAction {
|
|
562
|
+
if m.finished {
|
|
563
|
+
return nil
|
|
564
|
+
}
|
|
565
|
+
m.finished = true
|
|
566
|
+
// Nothing is happening once the turn is over; a stale description would leave the client
|
|
567
|
+
// showing "Reading ..." against an idle chat.
|
|
568
|
+
out := m.setActivity(nil)
|
|
569
|
+
resolved := outcome
|
|
570
|
+
if resolved == OutcomeDefault {
|
|
571
|
+
resolved = m.outcome
|
|
572
|
+
}
|
|
573
|
+
duration := m.duration()
|
|
574
|
+
switch resolved {
|
|
575
|
+
case OutcomeCancelled:
|
|
576
|
+
return append(out, ahptypes.StateAction{Value: &ahptypes.ChatTurnCancelledAction{Type: ahptypes.ActionTypeChatTurnCancelled, TurnId: m.turnID, Duration: duration}})
|
|
577
|
+
case OutcomeError:
|
|
578
|
+
msg := "The agent run failed"
|
|
579
|
+
switch {
|
|
580
|
+
case message != "":
|
|
581
|
+
msg = message
|
|
582
|
+
case m.errorMessage != nil:
|
|
583
|
+
msg = *m.errorMessage
|
|
584
|
+
}
|
|
585
|
+
return append(out, ahptypes.StateAction{Value: &ahptypes.ChatErrorAction{
|
|
586
|
+
Type: ahptypes.ActionTypeChatError, TurnId: m.turnID, Duration: duration,
|
|
587
|
+
Part: ahptypes.ErrorResponsePart{Kind: ahptypes.ResponsePartKindError, Error: ahptypes.ErrorInfo{ErrorType: "agentRunFailed", Message: msg}},
|
|
588
|
+
}})
|
|
589
|
+
default:
|
|
590
|
+
return append(out, ahptypes.StateAction{Value: &ahptypes.ChatTurnCompleteAction{Type: ahptypes.ActionTypeChatTurnComplete, TurnId: m.turnID, Duration: duration}})
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// UserTurnStarted builds the chat/turnStarted action for a user message.
|
|
595
|
+
func UserTurnStarted(turnID, text, startedAt string) ahptypes.StateAction {
|
|
596
|
+
return ahptypes.StateAction{Value: &ahptypes.ChatTurnStartedAction{
|
|
597
|
+
Type: ahptypes.ActionTypeChatTurnStarted, TurnId: turnID, StartedAt: startedAt,
|
|
598
|
+
Message: ahptypes.Message{Text: text, Origin: ahptypes.MessageOrigin{Kind: ahptypes.MessageKindUser}},
|
|
599
|
+
}}
|
|
600
|
+
}
|