rovecode 0.3.2 → 0.4.0-beta.2
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/CHANGELOG.md +512 -0
- package/README.md +33 -45
- package/bin/rovecode.js +24 -0
- package/dist/cli/app-dybnr56b.js +2 -0
- package/dist/cli/ask-user-p8hq4xgj.js +2 -0
- package/dist/cli/auth-login-ewpgw5sm.js +2 -0
- package/dist/cli/auth-m8p9grty.js +2 -0
- package/dist/cli/bench-xv3ypwev.js +9 -0
- package/dist/cli/catalog-737wb2s0.js +2 -0
- package/dist/cli/cli-arhg40m0.js +2 -0
- package/dist/cli/client-cf2pxx8q.js +2 -0
- package/dist/cli/commands-3p7e4xxs.js +2 -0
- package/dist/cli/connect-3q93d7cb.js +2 -0
- package/dist/cli/context-cmd-eqxmxhzq.js +2 -0
- package/dist/cli/context-report-hbw9zfes.js +2 -0
- package/dist/cli/count-remote-mby98cd0.js +2 -0
- package/dist/cli/design-122y0axd.js +2 -0
- package/dist/cli/dispatch-b4egzvvh.js +2 -0
- package/dist/cli/doctor-x4jkv72e.js +3 -0
- package/dist/cli/executor-ftvg6tsy.js +2 -0
- package/dist/cli/export-pdgdhkch.js +2 -0
- package/dist/cli/files-cez9a96p.js +2 -0
- package/dist/cli/gauntlet-r3xxaszc.js +2 -0
- package/dist/cli/gauntlet-runner-r515m7kk.js +10 -0
- package/dist/cli/gauntlet-wave3-bnkjk2v2.js +5 -0
- package/dist/cli/gauntlet-wave4-acs9s60q.js +14 -0
- package/dist/cli/hashline-ewg5hbe3.js +2 -0
- package/dist/cli/http-n0kehsk8.js +5 -0
- package/dist/cli/index-z5qt1s76.js +2 -0
- package/dist/cli/install-80mp63kx.js +2 -0
- package/dist/cli/loop-12twjcat.js +2 -0
- package/dist/cli/main-01pv9206.js +4 -0
- package/dist/cli/main-0jys2ccn.js +3 -0
- package/dist/cli/main-1ztz6fkj.js +10 -0
- package/dist/cli/main-23q7cmww.js +9 -0
- package/dist/cli/main-2rzbexn2.js +3 -0
- package/dist/cli/main-2wyax8k9.js +9 -0
- package/dist/cli/main-2yeveeve.js +6 -0
- package/dist/cli/main-2z3dek0b.js +3 -0
- package/dist/cli/main-2zgsknth.js +3 -0
- package/dist/cli/main-45ejth3a.js +4 -0
- package/dist/cli/main-45rn3trk.js +22 -0
- package/dist/cli/main-4p4e2w7x.js +4 -0
- package/dist/cli/main-4y0tnfpa.js +16 -0
- package/dist/cli/main-5py0rkmc.js +4 -0
- package/dist/cli/main-6dtqmbt6.js +7 -0
- package/dist/cli/main-6h9x282m.js +4 -0
- package/dist/cli/main-6vjeds42.js +3 -0
- package/dist/cli/main-78gq4bt9.js +6 -0
- package/dist/cli/main-7jd5vh3x.js +4 -0
- package/dist/cli/main-7kt6r53y.js +4 -0
- package/dist/cli/main-8c1tbazx.js +58 -0
- package/dist/cli/main-9a9rnh47.js +19 -0
- package/dist/cli/main-9ht36z12.js +3 -0
- package/dist/cli/main-a2yfvcy9.js +7 -0
- package/dist/cli/main-a3f51n0x.js +5 -0
- package/dist/cli/main-b8zq261k.js +3 -0
- package/dist/cli/main-bxtvnf6d.js +13 -0
- package/dist/cli/main-edxc3yzt.js +4 -0
- package/dist/cli/main-evgz4mp5.js +21 -0
- package/dist/cli/main-f33fc5je.js +9 -0
- package/dist/cli/main-fvnpq46y.js +12 -0
- package/dist/cli/main-gbbty4d4.js +3 -0
- package/dist/cli/main-gth53dnt.js +25 -0
- package/dist/cli/main-hqbz10aw.js +9 -0
- package/dist/cli/main-hrrvcfan.js +38 -0
- package/dist/cli/main-hzwtsb2m.js +5 -0
- package/dist/cli/main-j7ttv0sd.js +34 -0
- package/dist/cli/main-jak598k9.js +5 -0
- package/dist/cli/main-kba6zeyd.js +6 -0
- package/dist/cli/main-kwwsz6rq.js +3 -0
- package/dist/cli/main-m8vm17zq.js +3 -0
- package/dist/cli/main-mg4f96e1.js +3 -0
- package/dist/cli/main-mg9b20ac.js +18 -0
- package/dist/cli/main-mgb9ccnx.js +3 -0
- package/dist/cli/main-mjt2p7aj.js +3 -0
- package/dist/cli/main-n6qrdbmy.js +3 -0
- package/dist/cli/main-na7wse0x.js +5 -0
- package/dist/cli/main-nqveez48.js +4 -0
- package/dist/cli/main-ntqef02r.js +10 -0
- package/dist/cli/main-nvc3yjay.js +136 -0
- package/dist/cli/main-p0cfn6nr.js +16 -0
- package/dist/cli/main-qj2djy17.js +19 -0
- package/dist/cli/main-qsevpgsv.js +3 -0
- package/dist/cli/main-qvarybsp.js +3 -0
- package/dist/cli/main-rebtt91r.js +5 -0
- package/dist/cli/main-rpg7h8mb.js +3 -0
- package/dist/cli/main-rsy72qmw.js +15 -0
- package/dist/cli/main-rvetps99.js +18 -0
- package/dist/cli/main-s4bb0jav.js +3 -0
- package/dist/cli/main-s9v8k74e.js +3 -0
- package/dist/cli/main-tjvwmscs.js +3 -0
- package/dist/cli/main-tkgarpjj.js +4 -0
- package/dist/cli/main-v8y60bb2.js +3 -0
- package/dist/cli/main-vhrrq337.js +3 -0
- package/dist/cli/main-vp2dfb7s.js +4 -0
- package/dist/cli/main-vqbr22sz.js +8 -0
- package/dist/cli/main-vxnwe5xx.js +18 -0
- package/dist/cli/main-wgph00xf.js +5 -0
- package/dist/cli/main-wk2csfnj.js +5 -0
- package/dist/cli/main-wm997zjx.js +3 -0
- package/dist/cli/main-wpkyraxh.js +3 -0
- package/dist/cli/main-wqt32p5x.js +4 -0
- package/dist/cli/main-x9ct6y1a.js +3 -0
- package/dist/cli/main-xfekqh9m.js +7 -0
- package/dist/cli/main-xt9zc3n6.js +7 -0
- package/dist/cli/main-xx2z3zh5.js +4 -0
- package/dist/cli/main-y5c82rxr.js +3 -0
- package/dist/cli/main-yrjt2sqt.js +14 -0
- package/dist/cli/main-ys6zj3yr.js +3 -0
- package/dist/cli/main-ywbxshqc.js +8 -0
- package/dist/cli/main-z13755t8.js +25 -0
- package/dist/cli/main-zc7pyrbj.js +4 -0
- package/dist/cli/main.js +279 -0
- package/dist/cli/market-cmd-bm5xvn9f.js +5 -0
- package/dist/cli/mcp-login-bthtfpt7.js +2 -0
- package/dist/cli/mcp-market-cmd-mbeshfyd.js +2 -0
- package/dist/cli/notify-54v5z9dz.js +2 -0
- package/dist/cli/oauth-g5gme95c.js +2 -0
- package/dist/cli/output-satndjap.js +16 -0
- package/dist/cli/profiles-sfhpbq3m.js +2 -0
- package/dist/cli/provider-config-hv3xtdt4.js +2 -0
- package/dist/cli/provider-kwzq6g84.js +2 -0
- package/dist/cli/registry-fh0hdnyn.js +2 -0
- package/dist/cli/registry-y1y8e94r.js +2 -0
- package/dist/cli/repl-t4z03mqq.js +11 -0
- package/dist/cli/resume-fqt4chg8.js +2 -0
- package/dist/cli/run-flags-rysbag9t.js +2 -0
- package/dist/cli/runtime-j19fjbsa.js +2 -0
- package/dist/cli/sandbox-config-g4qxd7y5.js +2 -0
- package/dist/cli/server-r0b6bksk.js +5 -0
- package/dist/cli/session-arg-txmn5g4x.js +2 -0
- package/dist/cli/session-ed250d9j.js +2 -0
- package/dist/cli/sessions-cmd-adw7svfn.js +7 -0
- package/dist/cli/settings-y9rzcqx8.js +2 -0
- package/dist/cli/setup-jmbr11j0.js +2 -0
- package/dist/cli/sextant-smoke-tcth0vea.js +5 -0
- package/dist/cli/skills-cmd-zbdy99v6.js +2 -0
- package/dist/cli/smoke-1bg937kx.js +8 -0
- package/dist/cli/start-chat-p01cdks3.js +12 -0
- package/dist/cli/stream-4wmyaypz.js +2 -0
- package/dist/cli/task-eg4s093s.js +2 -0
- package/dist/cli/tasks-12v9rr9k.js +2 -0
- package/dist/cli/thinking-a5ngvqyh.js +2 -0
- package/dist/cli/todo-1wxpcecx.js +2 -0
- package/dist/cli/tools-2ftsya7w.js +2 -0
- package/dist/cli/tools-x1tj4fxm.js +2 -0
- package/dist/cli/trust-cmd-hccxehzb.js +2 -0
- package/dist/cli/update-check-ygt3vd7m.js +2 -0
- package/dist/cli/update-cmd-v23qhr8c.js +2 -0
- package/dist/cli/voice-g1gtck92.js +2 -0
- package/dist/cli/webfetch-0nnrjgb5.js +2 -0
- package/dist/cli/websearch-f0vr2p7d.js +2 -0
- package/dist/cli/workspace-9rq1w4ta.js +2 -0
- package/dist/lib/index.js +62 -0
- package/dist/lib/models-index.json +1 -0
- package/dist/lib/plugins.js +6 -0
- package/dist/lib/providers.js +17 -0
- package/dist/lib/public-api.js +20 -0
- package/dist/rovecode.exe +4 -0
- package/package.json +38 -11
- package/bin/rovecode.ts +0 -21
- package/src/acp/server.ts +0 -374
- package/src/cli/auth-login.ts +0 -122
- package/src/cli/connect.ts +0 -244
- package/src/cli/context-cmd.ts +0 -199
- package/src/cli/dispatch.ts +0 -82
- package/src/cli/doctor.ts +0 -362
- package/src/cli/export.ts +0 -276
- package/src/cli/help.ts +0 -293
- package/src/cli/is-tui-invocation.ts +0 -8
- package/src/cli/main.ts +0 -583
- package/src/cli/market-cmd.ts +0 -658
- package/src/cli/mcp-login.ts +0 -141
- package/src/cli/mcp-market-cmd.ts +0 -302
- package/src/cli/output.ts +0 -382
- package/src/cli/repl.ts +0 -250
- package/src/cli/repomap-root.ts +0 -14
- package/src/cli/resume.ts +0 -57
- package/src/cli/run-flags.ts +0 -43
- package/src/cli/run-limits.ts +0 -78
- package/src/cli/runtime.ts +0 -931
- package/src/cli/session-arg.ts +0 -30
- package/src/cli/sessions-cmd.ts +0 -145
- package/src/cli/setup.ts +0 -153
- package/src/cli/skills-cmd.ts +0 -194
- package/src/cli/start-chat.ts +0 -65
- package/src/cli/trust-cmd.ts +0 -52
- package/src/coding/bash.ts +0 -148
- package/src/coding/checkpoints.ts +0 -327
- package/src/coding/diff.ts +0 -138
- package/src/coding/files.ts +0 -341
- package/src/coding/hashline.ts +0 -274
- package/src/coding/lsp-gate.ts +0 -254
- package/src/coding/lsp-servers.ts +0 -147
- package/src/coding/lsp.ts +0 -283
- package/src/coding/repomap-cache.ts +0 -99
- package/src/coding/repomap-files.ts +0 -192
- package/src/coding/repomap.ts +0 -481
- package/src/core/agents.ts +0 -255
- package/src/core/compaction.ts +0 -259
- package/src/core/config.ts +0 -289
- package/src/core/context-report.ts +0 -228
- package/src/core/context.ts +0 -60
- package/src/core/count-remote.ts +0 -107
- package/src/core/execpolicy-rules.ts +0 -196
- package/src/core/execpolicy.ts +0 -385
- package/src/core/executor.ts +0 -454
- package/src/core/guardrails.ts +0 -400
- package/src/core/hooks.ts +0 -411
- package/src/core/images.ts +0 -230
- package/src/core/intro.ts +0 -266
- package/src/core/loop.ts +0 -567
- package/src/core/modes.ts +0 -372
- package/src/core/orchestrator.ts +0 -245
- package/src/core/proc-group.ts +0 -48
- package/src/core/project-trust.ts +0 -98
- package/src/core/reflection.ts +0 -165
- package/src/core/sandbox-config.ts +0 -186
- package/src/core/session-id.ts +0 -24
- package/src/core/session-images.ts +0 -73
- package/src/core/session-ops.ts +0 -183
- package/src/core/session-text.ts +0 -29
- package/src/core/session.ts +0 -469
- package/src/core/settings.ts +0 -170
- package/src/core/tasks.ts +0 -646
- package/src/core/token-scale.ts +0 -108
- package/src/core/tools.ts +0 -309
- package/src/core/trust.ts +0 -104
- package/src/core/types.ts +0 -330
- package/src/core/update-check.ts +0 -171
- package/src/core/usage.ts +0 -204
- package/src/core/validate.ts +0 -121
- package/src/core/verify-gate.ts +0 -159
- package/src/core/verify.ts +0 -236
- package/src/core/voice.ts +0 -158
- package/src/core/win-job.ts +0 -183
- package/src/core/workspace.ts +0 -184
- package/src/design/audit.ts +0 -797
- package/src/design/direction.ts +0 -190
- package/src/design/rules.ts +0 -157
- package/src/eval/bench.ts +0 -150
- package/src/eval/gauntlet-runner.ts +0 -215
- package/src/eval/gauntlet-support.ts +0 -84
- package/src/eval/gauntlet-wave3.ts +0 -269
- package/src/eval/gauntlet-wave4.ts +0 -217
- package/src/eval/gauntlet.ts +0 -253
- package/src/index.ts +0 -17
- package/src/lanes/agy.ts +0 -95
- package/src/lanes/approval.ts +0 -24
- package/src/lanes/claude.ts +0 -129
- package/src/lanes/codex.ts +0 -127
- package/src/lanes/events.ts +0 -130
- package/src/lanes/job.ts +0 -142
- package/src/lanes/opencode.ts +0 -122
- package/src/lanes/process.ts +0 -184
- package/src/lanes/progress.ts +0 -183
- package/src/lanes/registry.ts +0 -178
- package/src/lanes/runner.ts +0 -124
- package/src/lanes/types.ts +0 -112
- package/src/market/catalogs/mcp-docs.json +0 -111
- package/src/market/catalogs/plugins.json +0 -111
- package/src/market/catalogs/skills.json +0 -478
- package/src/market/clone.ts +0 -72
- package/src/market/context-cost.ts +0 -121
- package/src/market/digest.ts +0 -106
- package/src/market/index.ts +0 -22
- package/src/market/install.ts +0 -578
- package/src/market/manifest.ts +0 -187
- package/src/market/prereq.ts +0 -145
- package/src/market/registry.ts +0 -363
- package/src/market/resolve.ts +0 -111
- package/src/market/types.ts +0 -236
- package/src/market/validate.ts +0 -227
- package/src/mcp/client.ts +0 -449
- package/src/mcp/config.ts +0 -252
- package/src/mcp/local-package.ts +0 -211
- package/src/mcp/market-catalog.ts +0 -84
- package/src/mcp/market-install.ts +0 -289
- package/src/mcp/market.ts +0 -362
- package/src/mcp/oauth.ts +0 -251
- package/src/mcp/prompts-resources.ts +0 -249
- package/src/mcp/shared.ts +0 -149
- package/src/mcp/status.ts +0 -67
- package/src/mcp/tools.ts +0 -275
- package/src/mcp/transport.ts +0 -122
- package/src/mcp/trust.ts +0 -25
- package/src/memory/blocks.ts +0 -278
- package/src/memory/recall.ts +0 -355
- package/src/memory/scope.ts +0 -182
- package/src/memory/store.ts +0 -105
- package/src/memory/tools.ts +0 -99
- package/src/plugins/cli.ts +0 -119
- package/src/plugins/discover.ts +0 -108
- package/src/plugins/index.ts +0 -50
- package/src/plugins/install.ts +0 -184
- package/src/plugins/load.ts +0 -124
- package/src/plugins/manifest.ts +0 -92
- package/src/plugins/state.ts +0 -83
- package/src/providers/auth.ts +0 -408
- package/src/providers/cache.ts +0 -223
- package/src/providers/catalog-local.ts +0 -160
- package/src/providers/catalog.ts +0 -421
- package/src/providers/middleware-context.ts +0 -86
- package/src/providers/middleware.ts +0 -373
- package/src/providers/model-list.ts +0 -23
- package/src/providers/oauth/common.ts +0 -105
- package/src/providers/oauth/device-code.ts +0 -107
- package/src/providers/oauth/github-copilot.ts +0 -146
- package/src/providers/oauth/loopback.ts +0 -158
- package/src/providers/oauth/openai.ts +0 -163
- package/src/providers/oauth/openrouter.ts +0 -89
- package/src/providers/oauth/pkce.ts +0 -45
- package/src/providers/oauth/registry.ts +0 -39
- package/src/providers/oauth/seam.ts +0 -89
- package/src/providers/profile-glm53.ts +0 -111
- package/src/providers/profile-sonnet5-persona.ts +0 -65
- package/src/providers/profile-sonnet5-voice.ts +0 -23
- package/src/providers/profiles.ts +0 -156
- package/src/providers/provider-config.ts +0 -311
- package/src/providers/registry.ts +0 -333
- package/src/providers/responses.ts +0 -209
- package/src/providers/retry.ts +0 -234
- package/src/providers/router.ts +0 -294
- package/src/providers/sse.ts +0 -26
- package/src/providers/stream-errors.ts +0 -117
- package/src/providers/stream.ts +0 -566
- package/src/providers/thinking.ts +0 -189
- package/src/providers/wire-messages.ts +0 -129
- package/src/providers/wire-responses.ts +0 -79
- package/src/providers/wire-select.ts +0 -53
- package/src/server/http.ts +0 -291
- package/src/server/openapi.ts +0 -246
- package/src/sextant/card-hits.ts +0 -102
- package/src/sextant/card-keys.ts +0 -55
- package/src/sextant/context-source.ts +0 -157
- package/src/sextant/crew-cards.ts +0 -350
- package/src/sextant/draw-agents.ts +0 -273
- package/src/sextant/draw-code.ts +0 -388
- package/src/sextant/draw-context.ts +0 -222
- package/src/sextant/draw-frame.ts +0 -164
- package/src/sextant/draw-market.ts +0 -573
- package/src/sextant/draw-messages.ts +0 -386
- package/src/sextant/draw-pet.ts +0 -230
- package/src/sextant/draw-plan.ts +0 -187
- package/src/sextant/draw-tabs.ts +0 -85
- package/src/sextant/draw-util.ts +0 -65
- package/src/sextant/draw-wizard.ts +0 -378
- package/src/sextant/engine.ts +0 -230
- package/src/sextant/frame-hits.ts +0 -25
- package/src/sextant/frame.ts +0 -101
- package/src/sextant/git-status.ts +0 -197
- package/src/sextant/grid.ts +0 -59
- package/src/sextant/input.ts +0 -119
- package/src/sextant/keys.ts +0 -521
- package/src/sextant/layout.ts +0 -86
- package/src/sextant/local-commands.ts +0 -169
- package/src/sextant/market-source.ts +0 -287
- package/src/sextant/mentions.ts +0 -200
- package/src/sextant/message-hits.ts +0 -26
- package/src/sextant/model.ts +0 -387
- package/src/sextant/overlays.ts +0 -456
- package/src/sextant/panel-hits.ts +0 -38
- package/src/sextant/pet.ts +0 -399
- package/src/sextant/screen.ts +0 -324
- package/src/sextant/scroll-hits.ts +0 -66
- package/src/sextant/scrollbar.ts +0 -82
- package/src/sextant/sextant-bridge.ts +0 -174
- package/src/sextant/sextant-cards.ts +0 -142
- package/src/sextant/sextant-diff-base.ts +0 -63
- package/src/sextant/sextant-files.ts +0 -154
- package/src/sextant/sextant-frame-loop.ts +0 -335
- package/src/sextant/sextant-renderer.ts +0 -574
- package/src/sextant/sextant-repo.ts +0 -140
- package/src/sextant/theme.ts +0 -66
- package/src/sextant/tool-rows.ts +0 -189
- package/src/sextant/types.ts +0 -493
- package/src/skills/index.ts +0 -387
- package/src/skills/pack.ts +0 -220
- package/src/skills/spec.ts +0 -162
- package/src/skills/tools.ts +0 -69
- package/src/skills/versioned.ts +0 -227
- package/src/telemetry/otel-export.ts +0 -122
- package/src/telemetry/otel-lanes.ts +0 -89
- package/src/telemetry/otel-logs.ts +0 -131
- package/src/telemetry/otel-metrics.ts +0 -136
- package/src/telemetry/otel.ts +0 -397
- package/src/telemetry/otlp.ts +0 -76
- package/src/tools/ask-user.ts +0 -156
- package/src/tools/bash-bg.ts +0 -94
- package/src/tools/bash-jobs.ts +0 -237
- package/src/tools/design.ts +0 -151
- package/src/tools/evalcell.ts +0 -338
- package/src/tools/html-text.ts +0 -139
- package/src/tools/provider.ts +0 -149
- package/src/tools/task.ts +0 -250
- package/src/tools/todo.ts +0 -320
- package/src/tools/webfetch.ts +0 -332
- package/src/tools/websearch.ts +0 -359
- package/src/tui/agents-cmd.ts +0 -41
- package/src/tui/app.ts +0 -749
- package/src/tui/attach.ts +0 -127
- package/src/tui/boot-notes.ts +0 -41
- package/src/tui/builtin-prompts.ts +0 -59
- package/src/tui/checkpoints-cmd.ts +0 -70
- package/src/tui/clipboard-image.ts +0 -81
- package/src/tui/clipboard.ts +0 -78
- package/src/tui/commands.ts +0 -283
- package/src/tui/config-view.ts +0 -53
- package/src/tui/context-cmds.ts +0 -282
- package/src/tui/cost.ts +0 -108
- package/src/tui/crash-guard.ts +0 -173
- package/src/tui/focus-terminal.ts +0 -34
- package/src/tui/git-cmds.ts +0 -273
- package/src/tui/git-plain.ts +0 -58
- package/src/tui/info-cmd.ts +0 -150
- package/src/tui/input-plain.ts +0 -76
- package/src/tui/mcp-cmd.ts +0 -128
- package/src/tui/memory-note.ts +0 -77
- package/src/tui/modes-cmd.ts +0 -45
- package/src/tui/notify-seq.ts +0 -100
- package/src/tui/notify.ts +0 -318
- package/src/tui/overlays.ts +0 -97
- package/src/tui/pi-renderer.ts +0 -428
- package/src/tui/providers-cmd.ts +0 -377
- package/src/tui/reasoning-view.ts +0 -56
- package/src/tui/renderer.ts +0 -128
- package/src/tui/replay-marker.ts +0 -29
- package/src/tui/session-cmd.ts +0 -148
- package/src/tui/session-manage.ts +0 -95
- package/src/tui/sextant-attach.ts +0 -102
- package/src/tui/sextant-io.ts +0 -202
- package/src/tui/sextant-smoke.ts +0 -110
- package/src/tui/shell-cmd.ts +0 -158
- package/src/tui/smoke.ts +0 -72
- package/src/tui/staged-terminal.ts +0 -50
- package/src/tui/startup.ts +0 -12
- package/src/tui/theme.ts +0 -59
- package/src/tui/todo-label.ts +0 -7
- package/src/tui/trust-card.ts +0 -107
- package/src/tui/tui-commands.ts +0 -87
- package/tsconfig.json +0 -30
- package/vendor/pi-tui/LICENSE +0 -21
- package/vendor/pi-tui/PATCHES.md +0 -12
- package/vendor/pi-tui/PROVENANCE.md +0 -12
- package/vendor/pi-tui/README.upstream.md +0 -854
- package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/src/alt-screen-search.ts +0 -158
- package/vendor/pi-tui/src/autocomplete.ts +0 -827
- package/vendor/pi-tui/src/components/alt-screen-flash.ts +0 -52
- package/vendor/pi-tui/src/components/box.ts +0 -138
- package/vendor/pi-tui/src/components/cancellable-loader.ts +0 -41
- package/vendor/pi-tui/src/components/editor.ts +0 -2364
- package/vendor/pi-tui/src/components/h-stack.ts +0 -45
- package/vendor/pi-tui/src/components/image.ts +0 -128
- package/vendor/pi-tui/src/components/input.ts +0 -448
- package/vendor/pi-tui/src/components/loader.ts +0 -93
- package/vendor/pi-tui/src/components/markdown.ts +0 -1016
- package/vendor/pi-tui/src/components/scroll-view.ts +0 -217
- package/vendor/pi-tui/src/components/select-list.ts +0 -230
- package/vendor/pi-tui/src/components/settings-list.ts +0 -277
- package/vendor/pi-tui/src/components/spacer.ts +0 -29
- package/vendor/pi-tui/src/components/stack.ts +0 -155
- package/vendor/pi-tui/src/components/text.ts +0 -108
- package/vendor/pi-tui/src/components/truncated-text.ts +0 -66
- package/vendor/pi-tui/src/components/v-stack.ts +0 -34
- package/vendor/pi-tui/src/editor-component.ts +0 -75
- package/vendor/pi-tui/src/fuzzy.ts +0 -138
- package/vendor/pi-tui/src/index.ts +0 -149
- package/vendor/pi-tui/src/keybindings.ts +0 -321
- package/vendor/pi-tui/src/keys.ts +0 -1402
- package/vendor/pi-tui/src/kill-ring.ts +0 -47
- package/vendor/pi-tui/src/latex.ts +0 -1381
- package/vendor/pi-tui/src/layout-node.ts +0 -52
- package/vendor/pi-tui/src/layout.ts +0 -411
- package/vendor/pi-tui/src/native-modifiers.ts +0 -60
- package/vendor/pi-tui/src/native-module-path.ts +0 -32
- package/vendor/pi-tui/src/stdin-buffer.ts +0 -445
- package/vendor/pi-tui/src/terminal-colors.ts +0 -74
- package/vendor/pi-tui/src/terminal-image.ts +0 -701
- package/vendor/pi-tui/src/terminal.ts +0 -554
- package/vendor/pi-tui/src/tui-alt-screen.ts +0 -1379
- package/vendor/pi-tui/src/tui-main-screen.ts +0 -655
- package/vendor/pi-tui/src/tui.ts +0 -1264
- package/vendor/pi-tui/src/undo-stack.ts +0 -29
- package/vendor/pi-tui/src/utils.ts +0 -1327
- package/vendor/pi-tui/src/word-navigation.ts +0 -118
- package/vendor/pi-tui/test/test-themes.ts +0 -39
- package/vendor/pi-tui/test/virtual-terminal.ts +0 -219
- /package/{src/providers → dist/cli}/models-index.json +0 -0
package/src/telemetry/otel.ts
DELETED
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
/** PORT #39 — OpenTelemetry span export over the port-#29 hook seam (ADR-010: spans exportable as
|
|
2
|
-
* OTel; per-step token/latency/cost accounting). A consumer of core/hooks.ts, nothing more: it is a
|
|
3
|
-
* HookSet attached with `hooks.add(set, "otel")`, so the loop/tools/runner stay untouched.
|
|
4
|
-
*
|
|
5
|
-
* Naming: every span, event and attribute is `rovecode.*` and the resource service.name / scope name is
|
|
6
|
-
* `rovecode`. Ported on 2026-09-07 from the upstream harness's telemetry layer, where the same shapes
|
|
7
|
-
* carried that harness's own prefix; collectors keyed on those names must be re-pointed (no dual emission).
|
|
8
|
-
*
|
|
9
|
-
* Shape — ONE trace per run: `rovecode.run` (pre_run → post_run) ⊃ `rovecode.turn` (turn_start → turn_end,
|
|
10
|
-
* one per model iteration) ⊃ `rovecode.tool` (pre_tool → post_tool; parent = the turn that ISSUED the
|
|
11
|
-
* call). The loop yields turn_end BEFORE it executes the turn's calls (core/loop.ts:232 then :295),
|
|
12
|
-
* so a tool span starts after its parent turn ended — OTLP permits a child to outlive its parent and
|
|
13
|
-
* the run span brackets everything; keeping the turn = the model step keeps its latency honest.
|
|
14
|
-
* Per-step accounting reads the ONE source of usage, the session store (Message.usage/origin — the
|
|
15
|
-
* tui/cost.ts + cli/output.ts idiom): each turn span carries the tokens/cost of the assistant message
|
|
16
|
-
* it appended, the run span the sums since the run began; latency is the span itself. Compactions
|
|
17
|
-
* and calls that never reached pre_tool (policy deny, unknown tool, truncated) become span EVENTS on
|
|
18
|
-
* the current turn (`rovecode.compaction`, `rovecode.tool_call_failed`) — a zero-length "step" is noise as a
|
|
19
|
-
* span. Attribute policy follows pi (telemetry/README.md:387-389): ids, sizes and outcomes only —
|
|
20
|
-
* never the goal, tool args/output, headers or credentials (output_bytes, not output).
|
|
21
|
-
*
|
|
22
|
-
* Export — hand-encoded OTLP/HTTP JSON (ids as hex, int64 as decimal strings, enums as integers)
|
|
23
|
-
* through ONE exporter for three signals (otel-export.ts, port #82): the run's trace is POSTed ONCE at
|
|
24
|
-
* post_run to <base>/v1/traces (ROVECODE_OTEL_ENDPOINT with or without the suffix; ROVECODE_OTEL_HEADERS
|
|
25
|
-
* "k=v,k2=v2" ride along), then — same run, same order — the CUMULATIVE metrics to <base>/v1/metrics
|
|
26
|
-
* when anything was recorded (otel-metrics.ts: rovecode.tokens by type, rovecode.request.duration histogram
|
|
27
|
-
* per turn, rovecode.request.retries from the provider retry tap, rovecode.cost_usd when priced; totals since
|
|
28
|
-
* construction, never reset) and the run's approval log records to <base>/v1/logs when there were any
|
|
29
|
-
* (otel-logs.ts: the EXISTING `approval` member records each card and returns void — the human is still
|
|
30
|
-
* asked; allow/deny are observed from the tool events, a card still pending at post_run is `unanswered`).
|
|
31
|
-
* Every external lane (TaskInfo.kind "external", observed through TaskManager.subscribe via observeTasks)
|
|
32
|
-
* is ONE `rovecode.lane` root span in its own trace, POSTed when the task settles (otel-lanes.ts). Each POST
|
|
33
|
-
* is fire-and-forget on a 5s REF'D timer, tracked so flush() and session_close await the outstanding
|
|
34
|
-
* ones; post_run never blocks the run and the export path never throws. A failed export is ONE bounded
|
|
35
|
-
* warning: direct consumers pass onWarning; the runtime wiring has no write access to hooks.warnings, so
|
|
36
|
-
* the set remembers the failure and raises it from its NEXT lifecycle hook (pre_run of the following run,
|
|
37
|
-
* or session_close), which the runner's isolation records as exactly one note ("otel: … hook threw: OTLP
|
|
38
|
-
* export to … failed …") that every surface already streams. The raise happens after that hook's own
|
|
39
|
-
* work. A collector answering 404/405/501 on one signal switches THAT signal off after one note (the
|
|
40
|
-
* others continue); 5xx/timeout/network stay per-export warnings.
|
|
41
|
-
*
|
|
42
|
-
* ZERO OVERHEAD OFF — cli/runtime.ts calls createOtelHooks ONLY when ROVECODE_OTEL_ENDPOINT is set; off,
|
|
43
|
-
* no set is attached, the runner's tap short-circuits (hooks.ts:272), TaskManager.subscribe is never
|
|
44
|
-
* called by telemetry and the retry tap is one null check. otelDebug.constructed is the spy the off-path
|
|
45
|
-
* test pins ("exporter never constructed"); otelDebug.lanesObserved the same for observeTasks.
|
|
46
|
-
*
|
|
47
|
-
* Run boundaries (fix-wave 4, #39): post_run is the export trigger, and core/loop.ts fires it for a
|
|
48
|
-
* run whose CONSUMER closed the generator (serve disconnect, ACP cancel, TUI Esc: hooks.ts
|
|
49
|
-
* observer.close) with status "stopped" — cancelled runs export and release their RunState;
|
|
50
|
-
* session_close still drains a leftover state (a generator dropped without .return()) as "stopped"
|
|
51
|
-
* before its flush. Tool spans are keyed per issuing turn, so a call id a provider reuses across
|
|
52
|
-
* turns (the SSE adapter's `tc<idx>` fallback, providers/stream.ts) is one span PER TURN.
|
|
53
|
-
* `rovecode.tool_calls` counts ISSUED calls — dispatched (spans) plus never-dispatched (tool_call_failed
|
|
54
|
-
* events) — the same count as `rovecode run --output json` toolCalls (cli/output.ts keys per issuing turn
|
|
55
|
-
* too, LOW-B), with ONE residual: a run aborted while a call waited between its pre_tool hook and its
|
|
56
|
-
* execution (tools.ts:141 returns without an event) has that call as a span but no toolCalls entry —
|
|
57
|
-
* the hook side saw pre_tool, the event side saw nothing. A guard-stubbed call (tool events without a
|
|
58
|
-
* pre_tool) carries rovecode.failure_reason=loop_guard. An endpoint that is not an absolute http(s) URL
|
|
59
|
-
* (`http://`, `host:4318`) disables export with ONE note instead of a 5 s stall per run against host "v1".
|
|
60
|
-
*
|
|
61
|
-
* Sources (pi @ 853a80d, MIT — naming/shape reference, no code copied; header credit only):
|
|
62
|
-
* - packages/agent/src/harness/telemetry.ts:235-256 `pi.harness.run` (outcome attribute; status error
|
|
63
|
-
* when the run fails), :327-352 `pi.harness.turn` ("one assistant response and its tool batch",
|
|
64
|
-
* parent run), :399-451 `pi.harness.tool` (parents turn|run; `pi.tool.name`, `pi.tool.call_id`,
|
|
65
|
-
* `pi.tool.is_error`; status error when execution returns an error) → rovecode.run / rovecode.turn /
|
|
66
|
-
* rovecode.tool with rovecode.status, rovecode.turn, rovecode.tool, rovecode.call_id, rovecode.ok.
|
|
67
|
-
* - :94-103 `pi.ai.usage.{input,output,cache_read,cache_write}_tokens` + `.cost`, :88-92
|
|
68
|
-
* `pi.ai.response.stop_reason`, :55-64 `pi.ai.provider`/`pi.ai.model`, :194 `pi.session.id` →
|
|
69
|
-
* rovecode.tokens.{input,output,cacheRead,cacheWrite} (NormalizedUsage spelling), rovecode.cost_usd,
|
|
70
|
-
* rovecode.stop_reason, rovecode.model.provider/model, rovecode.session_id.
|
|
71
|
-
* Deviations: pi ships no exporter (telemetry/README.md:11 — adapter-owned) and records no timestamps
|
|
72
|
-
* (memory.ts:203-218); rovecode ships the OTLP/HTTP exporter and wall-clock ns times. The metric and log
|
|
73
|
-
* families' pattern sources are credited in otel-metrics.ts / otel-logs.ts. */
|
|
74
|
-
|
|
75
|
-
import type { HookCtx, HookSet, HookToolCall, RunResult } from "../core/hooks.ts";
|
|
76
|
-
import type { ApprovalRequest, Message, RunEvent, ToolOutput } from "../core/types.ts";
|
|
77
|
-
import { costUsd, type PricingRow } from "../core/usage.ts";
|
|
78
|
-
import { ModelCatalog } from "../providers/catalog.ts";
|
|
79
|
-
import type { RetryNote } from "../providers/retry.ts";
|
|
80
|
-
import { classifyStreamError } from "../providers/router.ts";
|
|
81
|
-
import { createOtlpExporter, hex, validEndpoint } from "./otel-export.ts";
|
|
82
|
-
import { observeLanes, type LaneSource } from "./otel-lanes.ts";
|
|
83
|
-
import { argsFingerprint, createApprovalLedger, encodeLogsRequest, type ApprovalIds } from "./otel-logs.ts";
|
|
84
|
-
import { createMetrics, DURATION_BOUNDS, type MetricAttrs } from "./otel-metrics.ts";
|
|
85
|
-
import { bool, dbl, encodeTraceRequest, int, str, type OtelSpan, type OtlpValue } from "./otlp.ts";
|
|
86
|
-
|
|
87
|
-
// wire types + encoder live in otlp.ts (pure), the pipe in otel-export.ts; re-exported so this stays the module consumers import
|
|
88
|
-
export { encodeTraceRequest, unixNano, type OtelSpan, type OtlpKeyValue, type OtlpSpan, type OtlpTraceRequest, type OtlpValue } from "./otlp.ts";
|
|
89
|
-
export { DEFAULT_EXPORT_TIMEOUT_MS, OTLP_TRACES_PATH, baseOf, normalizeEndpoint, signalUrl, validEndpoint, type OtlpSignal } from "./otel-export.ts";
|
|
90
|
-
export type { OtlpMetricsRequest } from "./otel-metrics.ts";
|
|
91
|
-
export type { OtlpLogsRequest } from "./otel-logs.ts";
|
|
92
|
-
|
|
93
|
-
/** test spy: exporters constructed / TaskManagers observed in this process — the off-path bar is "never" */
|
|
94
|
-
export const otelDebug = { constructed: 0, lanesObserved: 0 };
|
|
95
|
-
|
|
96
|
-
/** ModelCatalog.lookup's shape (cli/output.ts PricingSource); tests inject fixed pricing */
|
|
97
|
-
export interface PricingSource { lookup(provider: string, model: string): { pricing?: PricingRow } | undefined }
|
|
98
|
-
|
|
99
|
-
export interface OtelOptions {
|
|
100
|
-
/** collector base URL (…/v1/traces is appended) or the full traces URL */
|
|
101
|
-
endpoint: string;
|
|
102
|
-
headers?: Record<string, string>;
|
|
103
|
-
fetch?: typeof fetch;
|
|
104
|
-
/** wall clock in ms (fractions kept); default performance.timeOrigin + performance.now() */
|
|
105
|
-
now?: () => number;
|
|
106
|
-
/** resource service.name; default "rovecode" */
|
|
107
|
-
serviceName?: string;
|
|
108
|
-
/** live view of the session store — usage/origin per assistant message */
|
|
109
|
-
messages: () => Message[];
|
|
110
|
-
/** per-message pricing (Message.origin → row); default: a ModelCatalog built at the first export */
|
|
111
|
-
pricing?: PricingSource;
|
|
112
|
-
timeoutMs?: number;
|
|
113
|
-
/** export failures for direct consumers; absent → raised through the runner (header) */
|
|
114
|
-
onWarning?: (note: string) => void;
|
|
115
|
-
/** the lane seam (#47 external agentic-CLI lanes): is this `task start` agent value an external lane's adapter
|
|
116
|
-
* id? The approval record carries it as rovecode.lane. Default: nothing is a lane — a plain child agent must
|
|
117
|
-
* never be tagged as one — until the runtime injects src/lanes/types.ts isAdapterId (a follow-up outside this
|
|
118
|
-
* module, so neither side waits on the other to typecheck). */
|
|
119
|
-
isLane?: (agent: unknown) => agent is string;
|
|
120
|
-
}
|
|
121
|
-
export interface OtelHooks extends HookSet {
|
|
122
|
-
flush(): Promise<void>;
|
|
123
|
-
/** runs recorded but not yet exported — 0 once every run reached post_run (diagnostic seam) */
|
|
124
|
-
openRuns(): number;
|
|
125
|
-
/** #82: one `rovecode.lane` root span per external lane that ran (TaskManager.subscribe); returns the unsubscribe */
|
|
126
|
-
observeTasks(tasks: LaneSource): () => void;
|
|
127
|
-
/** #82: the provider retry tap (boot-provider.ts onRetry) → rovecode.request.retries */
|
|
128
|
-
recordRetry(note: RetryNote): void;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ---------- per-run state ----------
|
|
132
|
-
|
|
133
|
-
interface RunState {
|
|
134
|
-
run: OtelSpan; spans: OtelSpan[]; turn?: OtelSpan; lastTurn?: OtelSpan;
|
|
135
|
-
/** tool spans by `<issuing turn spanId>:<callId>` — a reused id is a new span in a new turn */
|
|
136
|
-
tools: Map<string, OtelSpan>;
|
|
137
|
-
/** #82: spans opened by pre_tool that have not executed yet → their args fingerprint (approval correlation) */
|
|
138
|
-
awaiting: Map<OtelSpan, string | null>;
|
|
139
|
-
/** issued calls: dispatched (spans) + never-dispatched (tool_call_failed events) */
|
|
140
|
-
calls: number;
|
|
141
|
-
/** store length at pre_run (run totals) and at the last turn_end (per-turn usage) */
|
|
142
|
-
baseline: number; seen: number;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const OK: OtelSpan["status"] = { code: 1 };
|
|
146
|
-
const ERROR = (message: string): OtelSpan["status"] => ({ code: 2, message });
|
|
147
|
-
const TOKEN_TYPES = ["input", "output", "cacheRead", "cacheWrite"] as const;
|
|
148
|
-
|
|
149
|
-
// ---------- env / endpoint / headers ----------
|
|
150
|
-
|
|
151
|
-
/** null unless ROVECODE_OTEL_ENDPOINT is set (blank = unset) — the runtime constructs nothing on null */
|
|
152
|
-
export function otelOptionsFromEnv(env: Record<string, string | undefined> = process.env): Pick<OtelOptions, "endpoint" | "headers"> | null {
|
|
153
|
-
const endpoint = (env["ROVECODE_OTEL_ENDPOINT"] ?? "").trim();
|
|
154
|
-
return endpoint ? { endpoint, headers: parseOtelHeaders(env["ROVECODE_OTEL_HEADERS"]) } : null;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** "k=v,k2=v2" (OTEL_EXPORTER_OTLP_HEADERS shape): first "=" splits, so values may contain "=";
|
|
158
|
-
* blank or key-less entries are skipped; values are taken raw (no percent-decoding). */
|
|
159
|
-
export function parseOtelHeaders(text: string | undefined): Record<string, string> {
|
|
160
|
-
const out: Record<string, string> = {};
|
|
161
|
-
for (const part of (text ?? "").split(",")) {
|
|
162
|
-
const eq = part.indexOf("=");
|
|
163
|
-
if (eq <= 0) continue;
|
|
164
|
-
const key = part.slice(0, eq).trim();
|
|
165
|
-
if (key) out[key] = part.slice(eq + 1).trim();
|
|
166
|
-
}
|
|
167
|
-
return out;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ---------- the hook set ----------
|
|
171
|
-
|
|
172
|
-
export function createOtelHooks(opts: OtelOptions): OtelHooks {
|
|
173
|
-
otelDebug.constructed++;
|
|
174
|
-
const now = opts.now ?? defaultNow;
|
|
175
|
-
const service = opts.serviceName ?? "rovecode";
|
|
176
|
-
const isLane = opts.isLane ?? ((_agent: unknown): _agent is string => false);
|
|
177
|
-
let pricing: PricingSource | undefined = opts.pricing;
|
|
178
|
-
const runs = new Map<string, RunState>();
|
|
179
|
-
let failed = 0, lastFailure = "";
|
|
180
|
-
const warn = (note: string): void => {
|
|
181
|
-
if (opts.onWarning) { opts.onWarning(note); return; }
|
|
182
|
-
failed++; lastFailure = note;
|
|
183
|
-
};
|
|
184
|
-
const exporter = createOtlpExporter({ endpoint: opts.endpoint, headers: opts.headers, fetch: opts.fetch, timeoutMs: opts.timeoutMs, warn });
|
|
185
|
-
const metrics = createMetrics(now()); // the cumulative window starts at construction (never reset)
|
|
186
|
-
const ledger = createApprovalLedger();
|
|
187
|
-
const unobserve: (() => void)[] = [];
|
|
188
|
-
// ONE note at construction (raised by the first lifecycle hook), then no POSTs
|
|
189
|
-
if (!validEndpoint(opts.endpoint)) warn(`ROVECODE_OTEL_ENDPOINT ${JSON.stringify(opts.endpoint)} is not an absolute http(s) URL — OTel export disabled`);
|
|
190
|
-
|
|
191
|
-
const stateOf = (ctx: HookCtx): RunState | undefined => (ctx.runId === undefined ? undefined : runs.get(ctx.runId));
|
|
192
|
-
const attach = (st: RunState, name: string, parent: OtelSpan): OtelSpan => {
|
|
193
|
-
const s: OtelSpan = { traceId: st.run.traceId, spanId: hex(8), parentSpanId: parent.spanId, name, start: now(), attrs: new Map(), events: [], status: { code: 0 } };
|
|
194
|
-
st.spans.push(s);
|
|
195
|
-
return s;
|
|
196
|
-
};
|
|
197
|
-
const end = (s: OtelSpan, status: OtelSpan["status"]): void => { if (s.end === undefined) { s.end = now(); s.status = status; } };
|
|
198
|
-
const event = (s: OtelSpan, name: string, attrs: [string, OtlpValue][]): void => { s.events.push({ name, time: now(), attrs: new Map(attrs) }); };
|
|
199
|
-
const strAttr = (s: OtelSpan, key: string): string | undefined => { const v = s.attrs.get(key); return v && "stringValue" in v ? v.stringValue : undefined; };
|
|
200
|
-
/** a call's parent = the turn that issued it (turn_end precedes the turn's tool events — header) */
|
|
201
|
-
const issuer = (st: RunState): OtelSpan => st.lastTurn ?? st.run;
|
|
202
|
-
const toolKey = (st: RunState, callId: string): string => `${issuer(st).spanId}:${callId}`;
|
|
203
|
-
/** the tool span for a call — opened by whichever of pre_tool / tool_execution_start arrives first;
|
|
204
|
-
* a span the START event has to open never saw pre_tool: the loop-guard stub path (tools.ts:88-93) */
|
|
205
|
-
const openTool = (st: RunState, callId: string, tool?: string, fromStart = false): OtelSpan => {
|
|
206
|
-
const key = toolKey(st, callId);
|
|
207
|
-
let s = st.tools.get(key);
|
|
208
|
-
if (!s) {
|
|
209
|
-
s = attach(st, "rovecode.tool", issuer(st));
|
|
210
|
-
s.attrs.set("rovecode.call_id", str(callId));
|
|
211
|
-
if (fromStart) s.attrs.set("rovecode.failure_reason", str("loop_guard"));
|
|
212
|
-
st.tools.set(key, s);
|
|
213
|
-
st.calls++;
|
|
214
|
-
}
|
|
215
|
-
if (tool !== undefined && !s.attrs.has("rovecode.tool")) s.attrs.set("rovecode.tool", str(tool));
|
|
216
|
-
return s;
|
|
217
|
-
};
|
|
218
|
-
const settleTool = (s: OtelSpan, ok: boolean, output: string): void => {
|
|
219
|
-
if (s.end !== undefined) return; // post_tool already settled it; tool_execution_end only adds duration
|
|
220
|
-
s.attrs.set("rovecode.ok", bool(ok));
|
|
221
|
-
s.attrs.set("rovecode.output_bytes", int(Buffer.byteLength(output, "utf8")));
|
|
222
|
-
end(s, ok ? OK : ERROR(`tool ${strAttr(s, "rovecode.tool") ?? "call"} failed`));
|
|
223
|
-
};
|
|
224
|
-
/** tokens summed + cost priced PER MESSAGE at Message.origin (cli/output.ts summarize): any
|
|
225
|
-
* usage-bearing message without catalog pricing → cost unknown → attribute omitted, never 0 */
|
|
226
|
-
const account = (s: OtelSpan, msgs: Message[]) => {
|
|
227
|
-
const u = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
228
|
-
let cost: number | null = 0;
|
|
229
|
-
for (const m of msgs) {
|
|
230
|
-
if (!m.usage) continue;
|
|
231
|
-
const n = { input: m.usage.input, output: m.usage.output, cacheRead: m.usage.cacheRead ?? 0, cacheWrite: m.usage.cacheWrite ?? 0 };
|
|
232
|
-
u.input += n.input; u.output += n.output; u.cacheRead += n.cacheRead; u.cacheWrite += n.cacheWrite;
|
|
233
|
-
if (cost === null || (n.input === 0 && n.output === 0 && n.cacheRead === 0 && n.cacheWrite === 0)) continue;
|
|
234
|
-
const row = m.origin ? (pricing ??= new ModelCatalog()).lookup(m.origin.provider, m.origin.model)?.pricing : undefined;
|
|
235
|
-
const c = row ? costUsd(n, row) : undefined;
|
|
236
|
-
cost = c === undefined ? null : cost + c;
|
|
237
|
-
}
|
|
238
|
-
for (const k of TOKEN_TYPES) s.attrs.set(`rovecode.tokens.${k}`, int(u[k]));
|
|
239
|
-
if (cost !== null) s.attrs.set("rovecode.cost_usd", dbl(cost));
|
|
240
|
-
const served = msgs.at(-1)?.origin; // the model that SERVED (router fallback may differ from the request)
|
|
241
|
-
if (served) { s.attrs.set("rovecode.model.provider", str(served.provider)); s.attrs.set("rovecode.model.model", str(served.model)); }
|
|
242
|
-
return { u, cost, served };
|
|
243
|
-
};
|
|
244
|
-
const modelAttrs = (m: { provider: string; model: string } | undefined): MetricAttrs => (m ? [["rovecode.model.provider", str(m.provider)], ["rovecode.model.model", str(m.model)]] : []);
|
|
245
|
-
/** #82 metrics: the turn's usage/cost into the cumulative Sums, its latency into the duration histogram */
|
|
246
|
-
const feedTurn = (t: OtelSpan, acc: ReturnType<typeof account>, stopReason: string): void => {
|
|
247
|
-
const model = modelAttrs(acc.served);
|
|
248
|
-
for (const k of TOKEN_TYPES) metrics.addSum("rovecode.tokens", "{token}", [["rovecode.token.type", str(k)], ...model], acc.u[k]);
|
|
249
|
-
if (acc.cost !== null) metrics.addSum("rovecode.cost_usd", "USD", model, acc.cost, true);
|
|
250
|
-
metrics.recordHistogram("rovecode.request.duration", "ms", DURATION_BOUNDS, [...model, ["rovecode.stop_reason", str(stopReason)]], (t.end ?? now()) - t.start);
|
|
251
|
-
};
|
|
252
|
-
// --- #82 approval correlation (otel-logs.ts header): pre_tool ran before the card, so the call's span is "awaiting" ---
|
|
253
|
-
const awaitingFor = (tool: string): number => { let n = 0; for (const st of runs.values()) for (const s of st.awaiting.keys()) if (strAttr(s, "rovecode.tool") === tool) n++; return n; };
|
|
254
|
-
/** the span executed (allow) or was refused (deny): answer the oldest matching card; ids only for a lone candidate */
|
|
255
|
-
const settleApproval = (st: RunState, s: OtelSpan, decision: "allow" | "deny"): void => {
|
|
256
|
-
const fp = st.awaiting.get(s);
|
|
257
|
-
if (fp === undefined) return; // not awaiting: already resolved, or opened by the start event (loop guard)
|
|
258
|
-
const tool = strAttr(s, "rovecode.tool");
|
|
259
|
-
if (tool === undefined || ledger.pendingFor(tool) === 0) { st.awaiting.delete(s); return; }
|
|
260
|
-
const lone = awaitingFor(tool) === 1; // this span is the ONLY same-tool span still awaiting execution
|
|
261
|
-
st.awaiting.delete(s);
|
|
262
|
-
const ids: ApprovalIds | undefined = lone ? { traceId: s.traceId, spanId: s.spanId, callId: strAttr(s, "rovecode.call_id") ?? "" } : undefined;
|
|
263
|
-
ledger.resolve(tool, fp, decision, now(), ids);
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
// --- export ---
|
|
267
|
-
/** deferred failure → thrown from a lifecycle hook → one runner warning (header); no-op with onWarning */
|
|
268
|
-
const raise = (): void => {
|
|
269
|
-
if (failed === 0) return;
|
|
270
|
-
const n = failed; failed = 0;
|
|
271
|
-
throw new Error(n === 1 ? lastFailure : `${lastFailure} (${n} exports failed)`);
|
|
272
|
-
};
|
|
273
|
-
const postMetrics = (): void => { if (metrics.dirty) exporter.post("metrics", JSON.stringify(metrics.encode(service, now()))); };
|
|
274
|
-
const postLogs = (): void => { const recs = ledger.take(); if (recs.length > 0) exporter.post("logs", JSON.stringify(encodeLogsRequest(service, recs))); };
|
|
275
|
-
/** the run's end: open turn/tool spans close here (status UNSET), totals + status land on the run
|
|
276
|
-
* span, ONE traces POST — then metrics (if anything was recorded) and the run's approval records */
|
|
277
|
-
const finish = (st: RunState, status: RunResult["status"]): void => {
|
|
278
|
-
const t = now();
|
|
279
|
-
for (const s of st.spans) if (s.end === undefined && s !== st.run) s.end = t;
|
|
280
|
-
st.run.attrs.set("rovecode.status", str(status));
|
|
281
|
-
st.run.attrs.set("rovecode.turns", int(st.spans.filter((s) => s.name === "rovecode.turn").length));
|
|
282
|
-
st.run.attrs.set("rovecode.tool_calls", int(st.calls));
|
|
283
|
-
account(st.run, opts.messages().slice(st.baseline).filter((m) => m.role === "assistant"));
|
|
284
|
-
st.run.end = t;
|
|
285
|
-
st.run.status = status === "done" || status === "stopped" ? OK : ERROR(status); // budget/error did not complete
|
|
286
|
-
exporter.post("traces", JSON.stringify(encodeTraceRequest(service, st.spans)));
|
|
287
|
-
ledger.drain(t); // a card still up when the run ended → unanswered (tools.ts:141: no event follows)
|
|
288
|
-
postMetrics(); postLogs();
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
return {
|
|
292
|
-
flush: () => exporter.flush(),
|
|
293
|
-
openRuns: () => runs.size,
|
|
294
|
-
observeTasks(tasks) {
|
|
295
|
-
otelDebug.lanesObserved++;
|
|
296
|
-
const off = observeLanes(tasks, (s) => exporter.post("traces", JSON.stringify(encodeTraceRequest(service, [s]))), now);
|
|
297
|
-
unobserve.push(off);
|
|
298
|
-
return off;
|
|
299
|
-
},
|
|
300
|
-
recordRetry(note) {
|
|
301
|
-
const status = classifyStreamError(note.reason).status; // the status only — never the reason text
|
|
302
|
-
metrics.addSum("rovecode.request.retries", "{retry}", [...modelAttrs(note.model), ...(status !== undefined ? [["rovecode.retry.status", int(status)] as const] : [])], 1);
|
|
303
|
-
},
|
|
304
|
-
pre_run(ctx) {
|
|
305
|
-
if (ctx.runId === undefined) return;
|
|
306
|
-
const run: OtelSpan = { traceId: hex(16), spanId: hex(8), name: "rovecode.run", start: now(), attrs: new Map(), events: [], status: { code: 0 } };
|
|
307
|
-
run.attrs.set("rovecode.session_id", str(ctx.sessionId)); run.attrs.set("rovecode.run_id", str(ctx.runId));
|
|
308
|
-
const len = opts.messages().length;
|
|
309
|
-
runs.set(ctx.runId, { run, spans: [run], tools: new Map(), awaiting: new Map(), calls: 0, baseline: len, seen: len });
|
|
310
|
-
raise();
|
|
311
|
-
},
|
|
312
|
-
on_event(ctx, ev: RunEvent) {
|
|
313
|
-
const st = stateOf(ctx);
|
|
314
|
-
if (!st) return;
|
|
315
|
-
switch (ev.type) {
|
|
316
|
-
case "turn_start": {
|
|
317
|
-
if (st.turn) end(st.turn, st.turn.status); // defensive: the loop never nests turns
|
|
318
|
-
st.turn = st.lastTurn = attach(st, "rovecode.turn", st.run);
|
|
319
|
-
st.turn.attrs.set("rovecode.turn", int(ev.turn));
|
|
320
|
-
break;
|
|
321
|
-
}
|
|
322
|
-
case "turn_end": {
|
|
323
|
-
const t = st.turn ?? st.lastTurn;
|
|
324
|
-
if (!t) break;
|
|
325
|
-
t.attrs.set("rovecode.stop_reason", str(ev.stopReason));
|
|
326
|
-
const msgs = opts.messages();
|
|
327
|
-
const acc = account(t, msgs.slice(st.seen).filter((m) => m.role === "assistant"));
|
|
328
|
-
st.seen = msgs.length;
|
|
329
|
-
end(t, ev.stopReason === "error" ? ERROR("error") : OK);
|
|
330
|
-
feedTurn(t, acc, ev.stopReason);
|
|
331
|
-
st.turn = undefined;
|
|
332
|
-
break;
|
|
333
|
-
}
|
|
334
|
-
case "tool_execution_start": settleApproval(st, openTool(st, ev.callId, ev.tool, true), "allow"); break;
|
|
335
|
-
case "tool_execution_end": {
|
|
336
|
-
const s = openTool(st, ev.callId);
|
|
337
|
-
st.awaiting.delete(s);
|
|
338
|
-
s.attrs.set("rovecode.duration_ms", int(ev.durationMs)); // the dispatcher's own measure (tools.ts:170)
|
|
339
|
-
settleTool(s, ev.ok, ev.output);
|
|
340
|
-
break;
|
|
341
|
-
}
|
|
342
|
-
case "tool_call_failed": {
|
|
343
|
-
const s = st.tools.get(toolKey(st, ev.callId));
|
|
344
|
-
if (s) {
|
|
345
|
-
if (ev.reason === "permission_denied") settleApproval(st, s, "deny"); else st.awaiting.delete(s);
|
|
346
|
-
s.attrs.set("rovecode.ok", bool(false)); s.attrs.set("rovecode.failure_reason", str(ev.reason)); end(s, ERROR(ev.reason));
|
|
347
|
-
} else { st.calls++; event(st.turn ?? st.lastTurn ?? st.run, "rovecode.tool_call_failed", [["rovecode.call_id", str(ev.callId)], ["rovecode.failure_reason", str(ev.reason)]]); }
|
|
348
|
-
break;
|
|
349
|
-
}
|
|
350
|
-
case "compaction":
|
|
351
|
-
event(st.turn ?? st.run, "rovecode.compaction", [
|
|
352
|
-
["rovecode.compaction.strategy", str(ev.strategy)], ...(ev.trigger ? [["rovecode.compaction.trigger", str(ev.trigger)] as [string, OtlpValue]] : []),
|
|
353
|
-
["rovecode.compaction.tokens_before", int(ev.tokensBefore)], ["rovecode.compaction.tokens_after", int(ev.tokensAfter)],
|
|
354
|
-
]);
|
|
355
|
-
break;
|
|
356
|
-
default: break; // run_start/run_end ride pre_run/post_run; deltas, progress notes and steers are not spans
|
|
357
|
-
}
|
|
358
|
-
},
|
|
359
|
-
pre_tool(ctx, call: HookToolCall) {
|
|
360
|
-
const st = stateOf(ctx);
|
|
361
|
-
if (!st) return;
|
|
362
|
-
const s = openTool(st, call.id, call.tool);
|
|
363
|
-
if (s.end === undefined && !s.attrs.has("rovecode.ok")) st.awaiting.set(s, argsFingerprint(call.args)); // awaiting execution until an event says otherwise
|
|
364
|
-
},
|
|
365
|
-
post_tool(ctx, call: HookToolCall, result: ToolOutput) {
|
|
366
|
-
const st = stateOf(ctx);
|
|
367
|
-
if (!st) return;
|
|
368
|
-
const s = openTool(st, call.id, call.tool);
|
|
369
|
-
settleApproval(st, s, "allow"); // the tool ran, so its card (if any) was allowed — post_tool precedes the buffered start event
|
|
370
|
-
settleTool(s, result.ok, result.output);
|
|
371
|
-
},
|
|
372
|
-
/** #82: records the card and returns void — the human (or the next set) still decides; the decision is observed from the events */
|
|
373
|
-
approval(ctx, req: ApprovalRequest) {
|
|
374
|
-
const args = req.revisedArgs;
|
|
375
|
-
const agent = req.tool === "task" && typeof args === "object" && args !== null ? (args as Record<string, unknown>)["agent"] : undefined;
|
|
376
|
-
const fp = argsFingerprint(req.args);
|
|
377
|
-
const known = fp !== null && [...runs.values()].some((st) => [...st.awaiting].some(([s, f]) => f === fp && strAttr(s, "rovecode.tool") === req.tool));
|
|
378
|
-
ledger.request({ tool: req.tool, sessionId: ctx.sessionId, ...(isLane(agent) ? { lane: agent } : {}), fp: known ? fp : null, at: now() });
|
|
379
|
-
},
|
|
380
|
-
post_run(ctx, result: RunResult) {
|
|
381
|
-
const st = stateOf(ctx);
|
|
382
|
-
if (!st) return;
|
|
383
|
-
runs.delete(ctx.runId!);
|
|
384
|
-
finish(st, result.status);
|
|
385
|
-
},
|
|
386
|
-
async session_close() {
|
|
387
|
-
// belt and braces (#39 MED-1): a run whose generator was dropped without .return() never reached
|
|
388
|
-
// post_run — export what it recorded as "stopped" (open spans close UNSET) instead of leaking it
|
|
389
|
-
for (const [id, st] of runs) { runs.delete(id); finish(st, "stopped"); }
|
|
390
|
-
for (const off of unobserve.splice(0)) off();
|
|
391
|
-
ledger.drain(now()); postMetrics(); postLogs(); // leftovers: retries after the last run, cards no run closed
|
|
392
|
-
await exporter.flush(); raise();
|
|
393
|
-
},
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const defaultNow = (): number => performance.timeOrigin + performance.now();
|
package/src/telemetry/otlp.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/** OTLP/HTTP JSON wire shapes + encoder for the port-#39 exporter (telemetry/otel.ts): the
|
|
2
|
-
* ExportTraceServiceRequest subset rovecode emits, the mutable in-memory span record it fills while a
|
|
3
|
-
* run is live, and the OTLP/JSON mapping rules — ids as hex, fixed64 times and int64 attributes as
|
|
4
|
-
* decimal strings, enums as integers. Pure: no I/O, no clock, no state. Split out of otel.ts at the
|
|
5
|
-
* 400-line cap (fix-wave 4); otel.ts re-exports everything here, so its public surface is unchanged.
|
|
6
|
-
* `kv`, `resourceAttrs` and `scopeOf` are exported so the metrics (otel-metrics.ts) and logs (otel-logs.ts)
|
|
7
|
-
* encoders share ONE resource/attribute encoding with the traces below. */
|
|
8
|
-
|
|
9
|
-
import pkg from "../../package.json";
|
|
10
|
-
|
|
11
|
-
// ---------- wire types (OTLP/HTTP JSON, ExportTraceServiceRequest) ----------
|
|
12
|
-
|
|
13
|
-
export type OtlpValue = { stringValue: string } | { intValue: string } | { doubleValue: number } | { boolValue: boolean };
|
|
14
|
-
export interface OtlpKeyValue { key: string; value: OtlpValue }
|
|
15
|
-
export interface OtlpSpan {
|
|
16
|
-
traceId: string; spanId: string; parentSpanId?: string; name: string; kind: number;
|
|
17
|
-
startTimeUnixNano: string; endTimeUnixNano: string;
|
|
18
|
-
attributes: OtlpKeyValue[];
|
|
19
|
-
events?: { name: string; timeUnixNano: string; attributes: OtlpKeyValue[] }[];
|
|
20
|
-
status: { code: number; message?: string };
|
|
21
|
-
}
|
|
22
|
-
export interface OtlpTraceRequest {
|
|
23
|
-
resourceSpans: {
|
|
24
|
-
resource: { attributes: OtlpKeyValue[] };
|
|
25
|
-
scopeSpans: { scope: { name: string; version?: string }; spans: OtlpSpan[] }[];
|
|
26
|
-
}[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** one recorded span; mutable until its run is exported (events arrive out of order — otel.ts header) */
|
|
30
|
-
export interface OtelSpan {
|
|
31
|
-
traceId: string; spanId: string; parentSpanId?: string; name: string;
|
|
32
|
-
start: number; end?: number;
|
|
33
|
-
attrs: Map<string, OtlpValue>;
|
|
34
|
-
events: { name: string; time: number; attrs: Map<string, OtlpValue> }[];
|
|
35
|
-
status: { code: 0 | 1 | 2; message?: string };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export const str = (v: string): OtlpValue => ({ stringValue: v });
|
|
39
|
-
export const int = (v: number): OtlpValue => ({ intValue: String(Math.trunc(v)) });
|
|
40
|
-
export const dbl = (v: number): OtlpValue => ({ doubleValue: v });
|
|
41
|
-
export const bool = (v: boolean): OtlpValue => ({ boolValue: v });
|
|
42
|
-
|
|
43
|
-
// ---------- encoding ----------
|
|
44
|
-
|
|
45
|
-
/** attribute map/tuples → the OTLP KeyValue list (shared by the three signal encoders) */
|
|
46
|
-
export const kv = (m: Iterable<[string, OtlpValue]>): OtlpKeyValue[] => [...m].map(([key, value]) => ({ key, value }));
|
|
47
|
-
/** the ONE resource every signal carries: service.name (default "rovecode") + service.version */
|
|
48
|
-
export const resourceAttrs = (serviceName: string): OtlpKeyValue[] => kv([["service.name", str(serviceName)], ["service.version", str(pkg.version)]]);
|
|
49
|
-
/** the ONE instrumentation scope every signal carries */
|
|
50
|
-
export const scopeOf = (): { name: string; version: string } => ({ name: "rovecode", version: pkg.version });
|
|
51
|
-
|
|
52
|
-
/** OTLP/JSON: ids hex (already), fixed64 times + int64 attrs as decimal strings, enums as integers */
|
|
53
|
-
export function encodeTraceRequest(serviceName: string, spans: readonly OtelSpan[]): OtlpTraceRequest {
|
|
54
|
-
return {
|
|
55
|
-
resourceSpans: [{
|
|
56
|
-
resource: { attributes: resourceAttrs(serviceName) },
|
|
57
|
-
scopeSpans: [{
|
|
58
|
-
scope: scopeOf(),
|
|
59
|
-
spans: spans.map((s) => ({
|
|
60
|
-
traceId: s.traceId, spanId: s.spanId, ...(s.parentSpanId ? { parentSpanId: s.parentSpanId } : {}),
|
|
61
|
-
name: s.name, kind: 1, // SPAN_KIND_INTERNAL
|
|
62
|
-
startTimeUnixNano: unixNano(s.start), endTimeUnixNano: unixNano(s.end ?? s.start),
|
|
63
|
-
attributes: kv(s.attrs),
|
|
64
|
-
...(s.events.length > 0 ? { events: s.events.map((e) => ({ name: e.name, timeUnixNano: unixNano(e.time), attributes: kv(e.attrs) })) } : {}),
|
|
65
|
-
status: s.status.message !== undefined ? { code: s.status.code, message: s.status.message } : { code: s.status.code },
|
|
66
|
-
})),
|
|
67
|
-
}],
|
|
68
|
-
}],
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** ms since epoch (fractions allowed) → exact unix-nano decimal string (no float rounding at 1e18) */
|
|
73
|
-
export function unixNano(ms: number): string {
|
|
74
|
-
const whole = Math.floor(ms);
|
|
75
|
-
return (BigInt(whole) * 1_000_000n + BigInt(Math.round((ms - whole) * 1e6))).toString();
|
|
76
|
-
}
|
package/src/tools/ask-user.ts
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
/** PORT #33 — ask_user: one clarifying question per call, answered by the interactive surface.
|
|
2
|
-
*
|
|
3
|
-
* The model asks a question (optional option labels + free text); the surface renders it and
|
|
4
|
-
* the user's answer returns as the tool result. Surfaces without a human (run/serve/acp) never
|
|
5
|
-
* bind an asker, so the tool fails CLOSED with an actionable error instead of hanging.
|
|
6
|
-
*
|
|
7
|
-
* Sources (research/source_snapshots):
|
|
8
|
-
* - opencode-2026 (MIT, ebece6e) packages/opencode/src/tool/question.ts:22-40 — the tool
|
|
9
|
-
* contract: prompts with option labels, answers folded back into the tool output as the
|
|
10
|
-
* selected label text (:30-36); packages/schema/src/v1/question.ts:15-28 — Option/Prompt
|
|
11
|
-
* shape incl. `custom` (typed answer allowed, default true); question.txt usage notes
|
|
12
|
-
* (recommended option first, no catch-all "Other" — the surface adds the free-text entry);
|
|
13
|
-
* packages/opencode/src/question/index.ts:29-33 — a dismissed question is an ERROR the
|
|
14
|
-
* model sees ("The user dismissed this question"), never a silent empty answer.
|
|
15
|
-
* - gemini-cli (Apache-2.0, 0bd1d43) packages/cli/src/config/config.ts:794-803 — headless
|
|
16
|
-
* behavior: with no human present ask_user is excluded and ASK_USER decisions translate to
|
|
17
|
-
* DENY. rovecode keeps the tool registered on every surface (one registry for all of them) and
|
|
18
|
-
* fails closed at execute time with a message that tells the model what to do instead.
|
|
19
|
-
* Deviations: ONE question per call (sequential: true) instead of a batch of up to 4; options
|
|
20
|
-
* are plain strings — the harness renders into 80 columns, not a web panel. No code copied.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import type { Tool, ToolContext, ToolOutput } from "../core/types.ts";
|
|
24
|
-
|
|
25
|
-
/** What the surface must render: the question, optional option labels, and whether a typed
|
|
26
|
-
* answer is accepted (default true — opencode `custom`, schema/v1/question.ts:26-28). */
|
|
27
|
-
export interface QuestionPrompt {
|
|
28
|
-
question: string;
|
|
29
|
-
options?: string[];
|
|
30
|
-
allowFreeText?: boolean;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** What the surface hands back: a picked option (index, label) or typed text. */
|
|
34
|
-
export interface QuestionAnswer {
|
|
35
|
-
choice?: number;
|
|
36
|
-
label?: string;
|
|
37
|
-
text?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** The surface seam. Resolves null when the user declines; MUST honor `signal` (dismiss on
|
|
41
|
-
* abort) — the tool races it regardless, so a deaf surface cannot pin the run. */
|
|
42
|
-
export type AskFn = (q: QuestionPrompt, signal: AbortSignal) => Promise<QuestionAnswer | null>;
|
|
43
|
-
|
|
44
|
-
export const MAX_OPTIONS = 8;
|
|
45
|
-
export const MAX_OPTION_CHARS = 80;
|
|
46
|
-
export const MAX_QUESTION_CHARS = 2000;
|
|
47
|
-
|
|
48
|
-
export const ASK_USER_UNAVAILABLE =
|
|
49
|
-
"ask_user unavailable: no interactive user in this session (headless run/serve/acp) — proceed with your best judgment or stop";
|
|
50
|
-
export const ASK_USER_ABORTED = "ask_user aborted";
|
|
51
|
-
export const ASK_USER_DECLINED = "user declined to answer";
|
|
52
|
-
|
|
53
|
-
/** Validate + normalize args. A string return is the error message (never throws). */
|
|
54
|
-
export function parseQuestion(args: unknown): QuestionPrompt | string {
|
|
55
|
-
const a = (args && typeof args === "object" ? args : {}) as Record<string, unknown>;
|
|
56
|
-
const question = typeof a.question === "string" ? a.question.trim() : "";
|
|
57
|
-
if (question === "") return "question must be a non-empty string";
|
|
58
|
-
if (question.length > MAX_QUESTION_CHARS) return `question too long (${question.length} chars, max ${MAX_QUESTION_CHARS})`;
|
|
59
|
-
let options: string[] | undefined;
|
|
60
|
-
if (a.options !== undefined && a.options !== null) {
|
|
61
|
-
if (!Array.isArray(a.options)) return "options must be an array of strings";
|
|
62
|
-
if (a.options.length > MAX_OPTIONS) return `too many options (${a.options.length}, max ${MAX_OPTIONS})`;
|
|
63
|
-
options = [];
|
|
64
|
-
for (const o of a.options) {
|
|
65
|
-
if (typeof o !== "string" || o.trim() === "") return "every option must be a non-empty string";
|
|
66
|
-
const label = o.trim();
|
|
67
|
-
if (label.length > MAX_OPTION_CHARS) return `option too long (${label.length} chars, max ${MAX_OPTION_CHARS}): ${label.slice(0, 40)}…`;
|
|
68
|
-
options.push(label);
|
|
69
|
-
}
|
|
70
|
-
if (options.length === 0) options = undefined;
|
|
71
|
-
}
|
|
72
|
-
if (a.allowFreeText !== undefined && typeof a.allowFreeText !== "boolean") return "allowFreeText must be a boolean";
|
|
73
|
-
const allowFreeText = a.allowFreeText !== false;
|
|
74
|
-
if (!options && !allowFreeText) return "nothing to answer with: provide options or allow free text";
|
|
75
|
-
return { question, ...(options ? { options } : {}), allowFreeText };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Fold the surface's answer into the model-facing result; garbage from a surface is an error. */
|
|
79
|
-
function renderAnswer(a: QuestionAnswer, q: QuestionPrompt): ToolOutput {
|
|
80
|
-
const text = typeof a.text === "string" ? a.text.trim() : "";
|
|
81
|
-
if (text !== "") return { ok: true, output: `answer: ${text}`, data: { text } };
|
|
82
|
-
const options = q.options ?? [];
|
|
83
|
-
if (typeof a.choice === "number" && Number.isInteger(a.choice) && a.choice >= 0 && a.choice < options.length) {
|
|
84
|
-
const label = options[a.choice]!;
|
|
85
|
-
return { ok: true, output: `answer: ${label}`, data: { choice: a.choice, label } };
|
|
86
|
-
}
|
|
87
|
-
return { ok: false, output: "ask_user failed: the surface returned an unusable answer" };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Build the tool over a GETTER for the asker, so a surface can bind/unbind after registration
|
|
91
|
-
* (runtime.setAskUser — the setBlockStore late-binding idiom). */
|
|
92
|
-
export function askUserTool(getAsk: () => AskFn | undefined): Tool {
|
|
93
|
-
return {
|
|
94
|
-
schema: {
|
|
95
|
-
name: "ask_user",
|
|
96
|
-
description:
|
|
97
|
-
"Ask the user ONE clarifying question and wait for the answer. Use it when instructions are " +
|
|
98
|
-
"ambiguous, a decision has real consequences, or you must pick between approaches — do not guess. " +
|
|
99
|
-
`Give up to ${MAX_OPTIONS} short options (≤${MAX_OPTION_CHARS} chars each); the user may also type a ` +
|
|
100
|
-
"free-text answer unless allowFreeText is false. If you recommend an option, list it first and append " +
|
|
101
|
-
"\"(recommended)\"; never add an \"Other\" option — the free-text entry covers it. The result is " +
|
|
102
|
-
"`answer: <chosen option or typed text>`. A declined or aborted question, or a session with no " +
|
|
103
|
-
"interactive user (headless run/serve/acp), returns an error: then proceed with your best judgment or stop.",
|
|
104
|
-
args: {
|
|
105
|
-
type: "object",
|
|
106
|
-
properties: {
|
|
107
|
-
question: { type: "string", description: "the complete question — clear, specific, one decision" },
|
|
108
|
-
options: {
|
|
109
|
-
type: "array", items: { type: "string" }, maxItems: MAX_OPTIONS,
|
|
110
|
-
description: `up to ${MAX_OPTIONS} answer choices, ≤${MAX_OPTION_CHARS} chars each (recommended one first)`,
|
|
111
|
-
},
|
|
112
|
-
allowFreeText: { type: "boolean", description: "also accept a typed free-form answer (default true)" },
|
|
113
|
-
},
|
|
114
|
-
required: ["question"],
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
// kind "read" → action file.read. Asking the human must never itself need approval: the gated
|
|
118
|
-
// default rules (runtime.ts buildCfg) allow file.read, prompt file.write/shell.exec/spawn/
|
|
119
|
-
// tool.mcp_call, and deny-default everything else — a "custom" kind would map to
|
|
120
|
-
// `tool.ask_user`, which no rule allows (denied), and plan mode's blanket `tool.* deny`
|
|
121
|
-
// (modes.ts planModeRules) would lock the question tool out of exactly the mode where
|
|
122
|
-
// clarifying questions matter most, while file.read is re-allowed there. Non-mutating, so no
|
|
123
|
-
// checkpoint (MUTATING_KINDS = write/execute). With no `path`/`command` arg the policy
|
|
124
|
-
// resource is the tool name, so `file.read ask_user` can still be targeted precisely
|
|
125
|
-
// (same reasoning as memory/recall.ts:282-288).
|
|
126
|
-
kind: "read",
|
|
127
|
-
sequential: true, // one question at a time — never interleaved with sibling tool output
|
|
128
|
-
async execute(args: unknown, ctx: ToolContext): Promise<ToolOutput> {
|
|
129
|
-
const prompt = parseQuestion(args);
|
|
130
|
-
if (typeof prompt === "string") return { ok: false, output: `ask_user failed: ${prompt}` };
|
|
131
|
-
const ask = getAsk();
|
|
132
|
-
if (!ask) return { ok: false, output: ASK_USER_UNAVAILABLE }; // headless: fail closed, say why
|
|
133
|
-
if (ctx.signal.aborted) return { ok: false, output: ASK_USER_ABORTED };
|
|
134
|
-
let onAbort: (() => void) | undefined;
|
|
135
|
-
const aborted = new Promise<"aborted">((res) => {
|
|
136
|
-
onAbort = () => res("aborted");
|
|
137
|
-
ctx.signal.addEventListener("abort", onAbort, { once: true });
|
|
138
|
-
});
|
|
139
|
-
try {
|
|
140
|
-
// RACED, not merely awaited: a surface that ignores the signal must not pin the run.
|
|
141
|
-
// The executor form invokes ask() synchronously and turns a sync throw into a rejection.
|
|
142
|
-
const asked = new Promise<QuestionAnswer | null>((res) => res(ask(prompt, ctx.signal)));
|
|
143
|
-
const winner = await Promise.race([asked.then((answer) => ({ answer })), aborted]);
|
|
144
|
-
// an answer landing in the same tick as the abort is moot — the run is going away
|
|
145
|
-
if (winner === "aborted" || ctx.signal.aborted) return { ok: false, output: ASK_USER_ABORTED };
|
|
146
|
-
if (winner.answer === null) return { ok: false, output: ASK_USER_DECLINED };
|
|
147
|
-
return renderAnswer(winner.answer, prompt);
|
|
148
|
-
} catch (e) {
|
|
149
|
-
// never throw across the tool seam (ADR-005): surface failures become typed output
|
|
150
|
-
return { ok: false, output: `ask_user failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
151
|
-
} finally {
|
|
152
|
-
if (onAbort) ctx.signal.removeEventListener("abort", onAbort);
|
|
153
|
-
}
|
|
154
|
-
},
|
|
155
|
-
};
|
|
156
|
-
}
|