rovecode 0.4.0-beta.3 → 0.4.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/README.md +57 -72
- package/THIRD_PARTY_NOTICES.md +0 -44
- package/bin/rovecode.ts +21 -0
- package/package.json +16 -38
- package/src/account/keys.ts +97 -0
- package/src/account/login.ts +158 -0
- package/src/account/provision.ts +47 -0
- package/src/account/store.ts +63 -0
- package/src/acp/server.ts +373 -0
- package/src/cli/account-cmd.ts +116 -0
- package/src/cli/connect.ts +244 -0
- package/src/cli/context-cmd.ts +199 -0
- package/src/cli/dispatch.ts +109 -0
- package/src/cli/doctor.ts +324 -0
- package/src/cli/export.ts +278 -0
- package/src/cli/help.ts +240 -0
- package/src/cli/is-tui-invocation.ts +8 -0
- package/src/cli/main.ts +599 -0
- package/src/cli/market-cmd.ts +658 -0
- package/src/cli/mcp-market-cmd.ts +299 -0
- package/src/cli/output.ts +382 -0
- package/src/cli/repl.ts +172 -0
- package/src/cli/resume.ts +32 -0
- package/src/cli/run-limits.ts +78 -0
- package/src/cli/runtime.ts +792 -0
- package/src/cli/setup.ts +187 -0
- package/src/cli/update-cmd.ts +78 -0
- package/src/cli/workflow-cmd.ts +100 -0
- package/src/coding/checkpoints.ts +270 -0
- package/src/coding/diff.ts +136 -0
- package/src/coding/files.ts +339 -0
- package/src/coding/hashline.ts +319 -0
- package/src/coding/lsp.ts +406 -0
- package/src/coding/repomap-cache.ts +99 -0
- package/src/coding/repomap-files.ts +110 -0
- package/src/coding/repomap.ts +392 -0
- package/src/core/compaction.ts +399 -0
- package/src/core/config.ts +289 -0
- package/src/core/context-report.ts +228 -0
- package/src/core/context.ts +60 -0
- package/src/core/count-remote.ts +107 -0
- package/src/core/execpolicy-rules.ts +196 -0
- package/src/core/execpolicy.ts +385 -0
- package/src/core/executor.ts +397 -0
- package/src/core/guardrails.ts +400 -0
- package/src/core/hooks.ts +398 -0
- package/src/core/images.ts +230 -0
- package/src/core/intro.ts +236 -0
- package/src/core/loop.ts +621 -0
- package/src/core/modes.ts +372 -0
- package/src/core/orchestrator.ts +207 -0
- package/src/core/reflection.ts +165 -0
- package/src/core/sandbox-config.ts +167 -0
- package/src/core/session-images.ts +73 -0
- package/src/core/session.ts +398 -0
- package/src/core/settings.ts +98 -0
- package/src/core/stuck-detector.ts +273 -0
- package/src/core/tasks.ts +374 -0
- package/src/core/token-scale.ts +108 -0
- package/src/core/tool-output-budget.ts +166 -0
- package/src/core/tools.ts +288 -0
- package/src/core/types.ts +330 -0
- package/src/core/update-check.ts +171 -0
- package/src/core/update.ts +158 -0
- package/src/core/usage.ts +204 -0
- package/src/core/validate.ts +121 -0
- package/src/core/verify-gate.ts +159 -0
- package/src/core/verify.ts +237 -0
- package/src/core/voice.ts +158 -0
- package/src/core/win-job.ts +183 -0
- package/src/design/audit.ts +797 -0
- package/src/design/direction.ts +190 -0
- package/src/design/rules.ts +157 -0
- package/src/eval/bench.ts +150 -0
- package/src/eval/gauntlet-runner.ts +218 -0
- package/src/eval/gauntlet.ts +226 -0
- package/src/eval/grader.ts +186 -0
- package/src/eval/record.ts +202 -0
- package/src/eval/redact.ts +141 -0
- package/src/eval/replay.ts +147 -0
- package/src/eval/trajectory.ts +373 -0
- package/src/index.ts +17 -0
- package/src/market/catalogs/mcp-docs.json +111 -0
- package/src/market/catalogs/plugins.json +111 -0
- package/src/market/catalogs/skills.json +478 -0
- package/src/market/clone.ts +72 -0
- package/src/market/context-cost.ts +121 -0
- package/src/market/digest.ts +106 -0
- package/src/market/index.ts +22 -0
- package/src/market/install.ts +578 -0
- package/src/market/manifest.ts +187 -0
- package/src/market/prereq.ts +145 -0
- package/src/market/registry.ts +363 -0
- package/src/market/resolve.ts +111 -0
- package/src/market/types.ts +236 -0
- package/src/market/validate.ts +227 -0
- package/src/mcp/client.ts +431 -0
- package/src/mcp/config.ts +239 -0
- package/src/mcp/local-package.ts +211 -0
- package/src/mcp/market-catalog.ts +84 -0
- package/src/mcp/market-install.ts +289 -0
- package/src/mcp/market.ts +0 -0
- package/src/mcp/tools.ts +131 -0
- package/src/mcp/trust.ts +49 -0
- package/src/memory/blocks.ts +175 -0
- package/src/memory/recall.ts +355 -0
- package/src/memory/store.ts +105 -0
- package/src/memory/tools.ts +99 -0
- package/src/plugins/cli.ts +123 -0
- package/src/plugins/discover.ts +108 -0
- package/src/plugins/index.ts +50 -0
- package/src/plugins/init.ts +140 -0
- package/src/plugins/install.ts +184 -0
- package/src/plugins/load.ts +149 -0
- package/src/plugins/manifest.ts +106 -0
- package/src/plugins/state.ts +83 -0
- package/src/providers/auth.ts +293 -0
- package/src/providers/cache.ts +223 -0
- package/src/providers/catalog-local.ts +160 -0
- package/src/providers/catalog.ts +408 -0
- package/src/providers/middleware-context.ts +86 -0
- package/src/providers/middleware.ts +373 -0
- package/src/providers/profile-glm53.ts +111 -0
- package/src/providers/profile-sonnet5-persona.ts +65 -0
- package/src/providers/profile-sonnet5-voice.ts +23 -0
- package/src/providers/profiles.ts +156 -0
- package/src/providers/provider-config.ts +311 -0
- package/src/providers/registry.ts +302 -0
- package/src/providers/response-validation.ts +80 -0
- package/src/providers/retry.ts +234 -0
- package/src/providers/router.ts +294 -0
- package/src/providers/sse.ts +26 -0
- package/src/providers/stream-errors.ts +117 -0
- package/src/providers/stream.ts +569 -0
- package/src/providers/thinking.ts +189 -0
- package/src/providers/wire-messages.ts +129 -0
- package/src/sdk/client.ts +225 -0
- package/src/sdk/index.ts +3 -0
- package/src/server/dashboard.ts +144 -0
- package/src/server/http.ts +343 -0
- package/src/server/openapi.ts +246 -0
- package/src/sextant/card-hits.ts +102 -0
- package/src/sextant/card-keys.ts +55 -0
- package/src/sextant/context-source.ts +157 -0
- package/src/sextant/draw-agents.ts +273 -0
- package/src/sextant/draw-code.ts +388 -0
- package/src/sextant/draw-context.ts +222 -0
- package/src/sextant/draw-frame.ts +164 -0
- package/src/sextant/draw-market.ts +573 -0
- package/src/sextant/draw-messages.ts +386 -0
- package/src/sextant/draw-pet.ts +230 -0
- package/src/sextant/draw-plan.ts +159 -0
- package/src/sextant/draw-tabs.ts +85 -0
- package/src/sextant/draw-util.ts +65 -0
- package/src/sextant/engine.ts +230 -0
- package/src/sextant/frame-hits.ts +25 -0
- package/src/sextant/frame.ts +101 -0
- package/src/sextant/git-status.ts +197 -0
- package/src/sextant/grid.ts +59 -0
- package/src/sextant/input.ts +119 -0
- package/src/sextant/keys.ts +488 -0
- package/src/sextant/layout.ts +86 -0
- package/src/sextant/local-commands.ts +156 -0
- package/src/sextant/market-source.ts +287 -0
- package/src/sextant/mentions.ts +141 -0
- package/src/sextant/message-hits.ts +26 -0
- package/src/sextant/model.ts +387 -0
- package/src/sextant/overlays.ts +451 -0
- package/src/sextant/panel-hits.ts +38 -0
- package/src/sextant/pet.ts +399 -0
- package/src/sextant/screen.ts +324 -0
- package/src/sextant/scroll-hits.ts +66 -0
- package/src/sextant/scrollbar.ts +82 -0
- package/src/sextant/selection.ts +123 -0
- package/src/sextant/sextant-bridge.ts +174 -0
- package/src/sextant/sextant-cards.ts +142 -0
- package/src/sextant/sextant-diff-base.ts +63 -0
- package/src/sextant/sextant-files.ts +154 -0
- package/src/sextant/sextant-frame-loop.ts +314 -0
- package/src/sextant/sextant-renderer.ts +478 -0
- package/src/sextant/sextant-repo.ts +131 -0
- package/src/sextant/theme.ts +66 -0
- package/src/sextant/tool-rows.ts +189 -0
- package/src/sextant/types.ts +473 -0
- package/src/skills/index.ts +306 -0
- package/src/skills/tools.ts +69 -0
- package/src/skills/versioned.ts +227 -0
- package/src/telemetry/otel.ts +353 -0
- package/src/telemetry/otlp.ts +68 -0
- package/src/tools/ask-user.ts +156 -0
- package/src/tools/design.ts +151 -0
- package/src/tools/evalcell.ts +338 -0
- package/src/tools/html-text.ts +139 -0
- package/src/tools/provider.ts +149 -0
- package/src/tools/task.ts +216 -0
- package/src/tools/todo.ts +320 -0
- package/src/tools/webfetch.ts +331 -0
- package/src/tui/app.ts +608 -0
- package/src/tui/attach.ts +127 -0
- package/src/tui/checkpoints-cmd.ts +70 -0
- package/src/tui/clipboard-image.ts +81 -0
- package/src/tui/commands.ts +277 -0
- package/src/tui/cost.ts +108 -0
- package/src/tui/info-cmd.ts +144 -0
- package/src/tui/mcp-cmd.ts +128 -0
- package/src/tui/modes-cmd.ts +45 -0
- package/src/tui/overlays.ts +97 -0
- package/src/tui/pi-renderer.ts +424 -0
- package/src/tui/providers-cmd.ts +366 -0
- package/src/tui/renderer.ts +101 -0
- package/src/tui/replay-marker.ts +29 -0
- package/src/tui/session-cmd.ts +146 -0
- package/src/tui/sextant-attach.ts +68 -0
- package/src/tui/sextant-io.ts +184 -0
- package/src/tui/sextant-smoke.ts +110 -0
- package/src/tui/smoke.ts +72 -0
- package/src/tui/theme.ts +59 -0
- package/src/tui/todo-label.ts +7 -0
- package/src/workflow/engine.ts +266 -0
- package/tsconfig.json +30 -0
- package/vendor/pi-tui/LICENSE +21 -0
- package/vendor/pi-tui/PATCHES.md +12 -0
- package/vendor/pi-tui/PROVENANCE.md +12 -0
- package/vendor/pi-tui/README.upstream.md +854 -0
- 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 +158 -0
- package/vendor/pi-tui/src/autocomplete.ts +827 -0
- package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
- package/vendor/pi-tui/src/components/box.ts +138 -0
- package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
- package/vendor/pi-tui/src/components/editor.ts +2364 -0
- package/vendor/pi-tui/src/components/h-stack.ts +45 -0
- package/vendor/pi-tui/src/components/image.ts +128 -0
- package/vendor/pi-tui/src/components/input.ts +448 -0
- package/vendor/pi-tui/src/components/loader.ts +93 -0
- package/vendor/pi-tui/src/components/markdown.ts +1016 -0
- package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
- package/vendor/pi-tui/src/components/select-list.ts +230 -0
- package/vendor/pi-tui/src/components/settings-list.ts +277 -0
- package/vendor/pi-tui/src/components/spacer.ts +29 -0
- package/vendor/pi-tui/src/components/stack.ts +155 -0
- package/vendor/pi-tui/src/components/text.ts +108 -0
- package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
- package/vendor/pi-tui/src/components/v-stack.ts +34 -0
- package/vendor/pi-tui/src/editor-component.ts +75 -0
- package/vendor/pi-tui/src/fuzzy.ts +138 -0
- package/vendor/pi-tui/src/index.ts +149 -0
- package/vendor/pi-tui/src/keybindings.ts +321 -0
- package/vendor/pi-tui/src/keys.ts +1402 -0
- package/vendor/pi-tui/src/kill-ring.ts +47 -0
- package/vendor/pi-tui/src/latex.ts +1381 -0
- package/vendor/pi-tui/src/layout-node.ts +52 -0
- package/vendor/pi-tui/src/layout.ts +411 -0
- package/vendor/pi-tui/src/native-modifiers.ts +60 -0
- package/vendor/pi-tui/src/native-module-path.ts +32 -0
- package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
- package/vendor/pi-tui/src/terminal-colors.ts +74 -0
- package/vendor/pi-tui/src/terminal-image.ts +701 -0
- package/vendor/pi-tui/src/terminal.ts +554 -0
- package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
- package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
- package/vendor/pi-tui/src/tui.ts +1264 -0
- package/vendor/pi-tui/src/undo-stack.ts +29 -0
- package/vendor/pi-tui/src/utils.ts +1327 -0
- package/vendor/pi-tui/src/word-navigation.ts +118 -0
- package/vendor/pi-tui/test/test-themes.ts +39 -0
- package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
- package/CHANGELOG.md +0 -527
- package/bin/rovecode.js +0 -24
- package/dist/cli/app-j6gn14w3.js +0 -2
- package/dist/cli/ask-user-cwstt8fz.js +0 -2
- package/dist/cli/auth-login-9bbp9915.js +0 -2
- package/dist/cli/auth-m8p9grty.js +0 -2
- package/dist/cli/bench-16zqdms5.js +0 -9
- package/dist/cli/catalog-1xchffa4.js +0 -2
- package/dist/cli/cli-1n1zb64f.js +0 -2
- package/dist/cli/client-2t9gjkck.js +0 -2
- package/dist/cli/commands-exafvm2b.js +0 -2
- package/dist/cli/connect-6zde0kn3.js +0 -2
- package/dist/cli/context-cmd-5t43wgqt.js +0 -2
- package/dist/cli/context-report-kt01pw8y.js +0 -2
- package/dist/cli/count-remote-ap7x3vh6.js +0 -2
- package/dist/cli/design-ne5zszyh.js +0 -2
- package/dist/cli/dispatch-2r5myxye.js +0 -2
- package/dist/cli/doctor-ws4fh4tn.js +0 -3
- package/dist/cli/executor-bdrjn634.js +0 -2
- package/dist/cli/export-1mxb9g5p.js +0 -2
- package/dist/cli/files-g104xghh.js +0 -2
- package/dist/cli/gauntlet-07xrjpj7.js +0 -2
- package/dist/cli/gauntlet-runner-xvy64436.js +0 -10
- package/dist/cli/gauntlet-wave3-jm91yt5w.js +0 -5
- package/dist/cli/gauntlet-wave4-r13py7p1.js +0 -14
- package/dist/cli/hashline-znvrat11.js +0 -2
- package/dist/cli/http-xafw6fsh.js +0 -143
- package/dist/cli/index-1sgjm25y.js +0 -2
- package/dist/cli/init-g2m0tn4m.js +0 -51
- package/dist/cli/install-avaqjjqq.js +0 -2
- package/dist/cli/loop-mmpfft01.js +0 -2
- package/dist/cli/main-0904f6ps.js +0 -5
- package/dist/cli/main-0ab9fc26.js +0 -9
- package/dist/cli/main-0jys2ccn.js +0 -3
- package/dist/cli/main-0mtcdbs7.js +0 -3
- package/dist/cli/main-0z1w2zsg.js +0 -3
- package/dist/cli/main-1dchs7xv.js +0 -18
- package/dist/cli/main-1ereejm1.js +0 -3
- package/dist/cli/main-1k1kw6b5.js +0 -3
- package/dist/cli/main-27y4sm2k.js +0 -38
- package/dist/cli/main-2wwjex5j.js +0 -58
- package/dist/cli/main-2yeveeve.js +0 -6
- package/dist/cli/main-2yfck9b5.js +0 -3
- package/dist/cli/main-2zmzgkwh.js +0 -3
- package/dist/cli/main-351pz3z7.js +0 -7
- package/dist/cli/main-3gjqfh7a.js +0 -6
- package/dist/cli/main-3nf3kgve.js +0 -3
- package/dist/cli/main-3pjrb2hd.js +0 -3
- package/dist/cli/main-3rxcvgna.js +0 -19
- package/dist/cli/main-4b3jgy66.js +0 -19
- package/dist/cli/main-4wndhjdc.js +0 -7
- package/dist/cli/main-4xcmvxnk.js +0 -3
- package/dist/cli/main-5tbz0wbz.js +0 -4
- package/dist/cli/main-5ywnwthm.js +0 -3
- package/dist/cli/main-6b62vkz0.js +0 -14
- package/dist/cli/main-6dnk69vp.js +0 -3
- package/dist/cli/main-6genrmhs.js +0 -136
- package/dist/cli/main-73g7eff4.js +0 -15
- package/dist/cli/main-7c5thhjd.js +0 -5
- package/dist/cli/main-7rn6bqje.js +0 -3
- package/dist/cli/main-80haw7qk.js +0 -4
- package/dist/cli/main-875s60s2.js +0 -4
- package/dist/cli/main-8kjxbpw4.js +0 -8
- package/dist/cli/main-90ds1z4e.js +0 -10
- package/dist/cli/main-9etavkew.js +0 -3
- package/dist/cli/main-a9njrkk1.js +0 -3
- package/dist/cli/main-aecrjq2d.js +0 -12
- package/dist/cli/main-ck9asesq.js +0 -9
- package/dist/cli/main-cta9racd.js +0 -4
- package/dist/cli/main-ddv7j2ag.js +0 -3
- package/dist/cli/main-dfreez27.js +0 -10
- package/dist/cli/main-f7rw7des.js +0 -3
- package/dist/cli/main-ggcn7rd7.js +0 -5
- package/dist/cli/main-gzkmycnv.js +0 -3
- package/dist/cli/main-hq51jg8v.js +0 -18
- package/dist/cli/main-jft389w9.js +0 -8
- package/dist/cli/main-k1eqkg83.js +0 -3
- package/dist/cli/main-k2y8a2aw.js +0 -9
- package/dist/cli/main-kcpbykxz.js +0 -4
- package/dist/cli/main-kd488vje.js +0 -22
- package/dist/cli/main-kh32yvgk.js +0 -5
- package/dist/cli/main-kqxnqjnv.js +0 -25
- package/dist/cli/main-kyn0xnsg.js +0 -3
- package/dist/cli/main-m1kk6fp5.js +0 -21
- package/dist/cli/main-mv40pcr2.js +0 -4
- package/dist/cli/main-n0t3973w.js +0 -3
- package/dist/cli/main-nqveez48.js +0 -4
- package/dist/cli/main-pknhvrmj.js +0 -3
- package/dist/cli/main-pn1w7a7j.js +0 -3
- package/dist/cli/main-prxxs70n.js +0 -4
- package/dist/cli/main-q3vsesf9.js +0 -3
- package/dist/cli/main-qsevpgsv.js +0 -3
- package/dist/cli/main-rdgdw24b.js +0 -25
- package/dist/cli/main-rfth4tbm.js +0 -16
- package/dist/cli/main-rg0wn0xf.js +0 -5
- package/dist/cli/main-sdmxhtv8.js +0 -4
- package/dist/cli/main-skbp13js.js +0 -18
- package/dist/cli/main-t4xnd213.js +0 -7
- package/dist/cli/main-vqak588n.js +0 -4
- package/dist/cli/main-w2n1303f.js +0 -9
- package/dist/cli/main-wbrdspr2.js +0 -5
- package/dist/cli/main-wsrg79c1.js +0 -7
- package/dist/cli/main-x4r0fne4.js +0 -5
- package/dist/cli/main-xea2f3tn.js +0 -6
- package/dist/cli/main-xg704a3c.js +0 -3
- package/dist/cli/main-xvnrabfp.js +0 -16
- package/dist/cli/main-xy53xf0r.js +0 -4
- package/dist/cli/main-y1fqy60y.js +0 -3
- package/dist/cli/main-yn8cd281.js +0 -34
- package/dist/cli/main-yr0ksc0h.js +0 -4
- package/dist/cli/main-z2ex2vyf.js +0 -4
- package/dist/cli/main-z3aayzvq.js +0 -3
- package/dist/cli/main-zaqh35jg.js +0 -3
- package/dist/cli/main-zc2e8e46.js +0 -4
- package/dist/cli/main-zzrfw6cf.js +0 -13
- package/dist/cli/main.js +0 -280
- package/dist/cli/market-cmd-e14kmx9n.js +0 -5
- package/dist/cli/mcp-login-wq7ktdek.js +0 -2
- package/dist/cli/mcp-market-cmd-9mg3jecy.js +0 -2
- package/dist/cli/notify-b7qc0cjb.js +0 -2
- package/dist/cli/oauth-z8whcgfx.js +0 -2
- package/dist/cli/output-b3ewj3ps.js +0 -16
- package/dist/cli/profiles-6mr5he5e.js +0 -2
- package/dist/cli/provider-config-g7j42q8x.js +0 -2
- package/dist/cli/provider-jr1y8vvm.js +0 -2
- package/dist/cli/registry-s8yk86g0.js +0 -2
- package/dist/cli/registry-t6p8d4mn.js +0 -2
- package/dist/cli/repl-bajwe1mh.js +0 -11
- package/dist/cli/resume-rwn9nz7y.js +0 -2
- package/dist/cli/run-flags-nah7ndpt.js +0 -2
- package/dist/cli/runtime-n7gafzhb.js +0 -2
- package/dist/cli/sandbox-config-emdy18x4.js +0 -2
- package/dist/cli/server-b0nvs2bn.js +0 -5
- package/dist/cli/session-arg-y75wd4kj.js +0 -2
- package/dist/cli/session-j62evmjq.js +0 -2
- package/dist/cli/sessions-cmd-tsnwz0ns.js +0 -7
- package/dist/cli/settings-df10wfez.js +0 -2
- package/dist/cli/setup-jzvv72fg.js +0 -2
- package/dist/cli/sextant-smoke-37m81ke6.js +0 -5
- package/dist/cli/skills-cmd-gjxnxnhx.js +0 -2
- package/dist/cli/smoke-p7748apt.js +0 -8
- package/dist/cli/start-chat-s4st3mm0.js +0 -12
- package/dist/cli/stream-gmeyewds.js +0 -2
- package/dist/cli/task-gh0kkp3n.js +0 -2
- package/dist/cli/tasks-z1kfpe8e.js +0 -2
- package/dist/cli/thinking-0eqkrz6t.js +0 -2
- package/dist/cli/todo-5brcrt9m.js +0 -2
- package/dist/cli/tools-7pzm0vj9.js +0 -2
- package/dist/cli/tools-s635p6s8.js +0 -2
- package/dist/cli/trust-cmd-cjav8zgm.js +0 -2
- package/dist/cli/update-check-pt31bm2f.js +0 -2
- package/dist/cli/update-cmd-tk131s9t.js +0 -2
- package/dist/cli/voice-56nabd8d.js +0 -2
- package/dist/cli/webfetch-xd8q596m.js +0 -2
- package/dist/cli/websearch-5hkf98k1.js +0 -2
- package/dist/cli/workflow-cmd-cy3cvzjp.js +0 -4
- package/dist/cli/workspace-q10g5z3e.js +0 -2
- package/dist/lib/index.js +0 -62
- package/dist/lib/models-index.json +0 -1
- package/dist/lib/plugins.js +0 -55
- package/dist/lib/providers.js +0 -17
- package/dist/lib/public-api.js +0 -20
- package/dist/lib/sdk.js +0 -360
- /package/{dist/cli → src/providers}/models-index.json +0 -0
|
@@ -0,0 +1,353 @@
|
|
|
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
|
+
* Shape — ONE trace per run: `rovecode.run` (pre_run → post_run) ⊃ `rovecode.turn` (turn_start → turn_end,
|
|
6
|
+
* one per model iteration) ⊃ `rovecode.tool` (pre_tool → post_tool; parent = the turn that ISSUED the
|
|
7
|
+
* call). The loop yields turn_end BEFORE it executes the turn's calls (core/loop.ts:232 then :295),
|
|
8
|
+
* so a tool span starts after its parent turn ended — OTLP permits a child to outlive its parent and
|
|
9
|
+
* the run span brackets everything; keeping the turn = the model step keeps its latency honest.
|
|
10
|
+
* Per-step accounting reads the ONE source of usage, the session store (Message.usage/origin — the
|
|
11
|
+
* tui/cost.ts + cli/output.ts idiom): each turn span carries the tokens/cost of the assistant message
|
|
12
|
+
* it appended, the run span the sums since the run began; latency is the span itself. Compactions
|
|
13
|
+
* and calls that never reached pre_tool (policy deny, unknown tool, truncated) become span EVENTS on
|
|
14
|
+
* the current turn (`rovecode.compaction`, `rovecode.tool_call_failed`) — a zero-length "step" is noise as a
|
|
15
|
+
* span. Attribute policy follows pi (telemetry/README.md:387-389): ids, sizes and outcomes only —
|
|
16
|
+
* never the goal, tool args/output, headers or credentials (output_bytes, not output).
|
|
17
|
+
*
|
|
18
|
+
* Export — hand-encoded OTLP/HTTP JSON (ExportTraceServiceRequest; OTLP/JSON mapping: ids as hex,
|
|
19
|
+
* int64 as decimal strings, enums as integers) POSTed ONCE per run at post_run to <endpoint>/v1/traces
|
|
20
|
+
* (ROVECODE_OTEL_ENDPOINT with or without the suffix; ROVECODE_OTEL_HEADERS "k=v,k2=v2" ride along). The POST
|
|
21
|
+
* is fire-and-forget on a 5s REF'D timer (hooks.ts withTimeout idiom — Bun unrefs AbortSignal.timeout),
|
|
22
|
+
* tracked so flush() and session_close await the outstanding ones; post_run never blocks the run and
|
|
23
|
+
* the export path never throws. A failed export is ONE bounded warning: direct consumers pass
|
|
24
|
+
* onWarning; the runtime wiring has no write access to hooks.warnings, so the set remembers the
|
|
25
|
+
* failure and raises it from its NEXT lifecycle hook (pre_run of the following run, or session_close),
|
|
26
|
+
* which the runner's isolation records as exactly one note ("otel: … hook threw: OTLP export to …
|
|
27
|
+
* failed …") that every surface already streams. The raise happens after that hook's own work.
|
|
28
|
+
*
|
|
29
|
+
* ZERO OVERHEAD OFF — cli/runtime.ts calls createOtelHooks ONLY when ROVECODE_OTEL_ENDPOINT is set; off,
|
|
30
|
+
* no set is attached and the runner's tap short-circuits (hooks.ts:272). otelDebug.constructed is the
|
|
31
|
+
* spy the off-path test pins ("exporter never constructed").
|
|
32
|
+
*
|
|
33
|
+
* Run boundaries (fix-wave 4, #39): post_run is the export trigger, and core/loop.ts fires it for a
|
|
34
|
+
* run whose CONSUMER closed the generator (serve disconnect, ACP cancel, TUI Esc: hooks.ts
|
|
35
|
+
* observer.close) with status "stopped" — cancelled runs export and release their RunState;
|
|
36
|
+
* session_close still drains a leftover state (a generator dropped without .return()) as "stopped"
|
|
37
|
+
* before its flush. Tool spans are keyed per issuing turn, so a call id a provider reuses across
|
|
38
|
+
* turns (the SSE adapter's `tc<idx>` fallback, providers/stream.ts) is one span PER TURN.
|
|
39
|
+
* `rovecode.tool_calls` counts ISSUED calls — dispatched (spans) plus never-dispatched (tool_call_failed
|
|
40
|
+
* events) — the same count as `rovecode run --output json` toolCalls (cli/output.ts keys per issuing turn
|
|
41
|
+
* too, LOW-B), with ONE residual: a run aborted while a call waited between its pre_tool hook and its
|
|
42
|
+
* execution (tools.ts:141 returns without an event) has that call as a span but no toolCalls entry —
|
|
43
|
+
* the hook side saw pre_tool, the event side saw nothing. A guard-stubbed
|
|
44
|
+
* call (tool events without a pre_tool) carries rovecode.failure_reason=loop_guard. An endpoint that is
|
|
45
|
+
* not an absolute http(s) URL (`http://`, `host:4318`) disables export with ONE note instead of a
|
|
46
|
+
* 5 s stall per run against host "v1".
|
|
47
|
+
*
|
|
48
|
+
* Sources (pi @ 853a80d, MIT — naming/shape reference, no code copied; header credit only):
|
|
49
|
+
* - packages/agent/src/harness/telemetry.ts:235-256 `pi.harness.run` (outcome attribute; status error
|
|
50
|
+
* when the run fails), :327-352 `pi.harness.turn` ("one assistant response and its tool batch",
|
|
51
|
+
* parent run), :399-451 `pi.harness.tool` (parents turn|run; `pi.tool.name`, `pi.tool.call_id`,
|
|
52
|
+
* `pi.tool.is_error`; status error when execution returns an error) → rovecode.run / rovecode.turn /
|
|
53
|
+
* rovecode.tool with rovecode.status, rovecode.turn, rovecode.tool, rovecode.call_id, rovecode.ok.
|
|
54
|
+
* - :94-103 `pi.ai.usage.{input,output,cache_read,cache_write}_tokens` + `.cost`, :88-92
|
|
55
|
+
* `pi.ai.response.stop_reason`, :55-64 `pi.ai.provider`/`pi.ai.model`, :194 `pi.session.id` →
|
|
56
|
+
* rovecode.tokens.{input,output,cacheRead,cacheWrite} (NormalizedUsage spelling), rovecode.cost_usd,
|
|
57
|
+
* rovecode.stop_reason, rovecode.model.provider/model, rovecode.session_id.
|
|
58
|
+
* Deviations: pi ships no exporter (telemetry/README.md:11 — adapter-owned) and records no timestamps
|
|
59
|
+
* (memory.ts:203-218); rovecode ships the OTLP/HTTP exporter and wall-clock ns times. */
|
|
60
|
+
|
|
61
|
+
import { randomBytes } from "node:crypto";
|
|
62
|
+
import type { HookCtx, HookSet, HookToolCall, RunResult } from "../core/hooks.ts";
|
|
63
|
+
import type { Message, RunEvent, ToolOutput } from "../core/types.ts";
|
|
64
|
+
import { costUsd, type PricingRow } from "../core/usage.ts";
|
|
65
|
+
import { ModelCatalog } from "../providers/catalog.ts";
|
|
66
|
+
import { bool, dbl, encodeTraceRequest, int, str, type OtelSpan, type OtlpValue } from "./otlp.ts";
|
|
67
|
+
|
|
68
|
+
// wire types + encoder live in otlp.ts (pure); re-exported so this stays the module consumers import
|
|
69
|
+
export { encodeTraceRequest, unixNano, type OtelSpan, type OtlpKeyValue, type OtlpSpan, type OtlpTraceRequest, type OtlpValue } from "./otlp.ts";
|
|
70
|
+
|
|
71
|
+
export const DEFAULT_EXPORT_TIMEOUT_MS = 5000;
|
|
72
|
+
export const OTLP_TRACES_PATH = "/v1/traces";
|
|
73
|
+
/** test spy: exporters constructed in this process — the off-path bar is "never constructed" */
|
|
74
|
+
export const otelDebug = { constructed: 0 };
|
|
75
|
+
|
|
76
|
+
/** ModelCatalog.lookup's shape (cli/output.ts PricingSource); tests inject fixed pricing */
|
|
77
|
+
export interface PricingSource { lookup(provider: string, model: string): { pricing?: PricingRow } | undefined }
|
|
78
|
+
|
|
79
|
+
export interface OtelOptions {
|
|
80
|
+
/** collector base URL (…/v1/traces is appended) or the full traces URL */
|
|
81
|
+
endpoint: string;
|
|
82
|
+
headers?: Record<string, string>;
|
|
83
|
+
fetch?: typeof fetch;
|
|
84
|
+
/** wall clock in ms (fractions kept); default performance.timeOrigin + performance.now() */
|
|
85
|
+
now?: () => number;
|
|
86
|
+
/** resource service.name; default "rovecode" */
|
|
87
|
+
serviceName?: string;
|
|
88
|
+
/** live view of the session store — usage/origin per assistant message */
|
|
89
|
+
messages: () => Message[];
|
|
90
|
+
/** per-message pricing (Message.origin → row); default: a ModelCatalog built at the first export */
|
|
91
|
+
pricing?: PricingSource;
|
|
92
|
+
timeoutMs?: number;
|
|
93
|
+
/** export failures for direct consumers; absent → raised through the runner (header) */
|
|
94
|
+
onWarning?: (note: string) => void;
|
|
95
|
+
}
|
|
96
|
+
export interface OtelHooks extends HookSet {
|
|
97
|
+
flush(): Promise<void>;
|
|
98
|
+
/** runs recorded but not yet exported — 0 once every run reached post_run (diagnostic seam) */
|
|
99
|
+
openRuns(): number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------- per-run state ----------
|
|
103
|
+
|
|
104
|
+
interface RunState {
|
|
105
|
+
run: OtelSpan; spans: OtelSpan[]; turn?: OtelSpan; lastTurn?: OtelSpan;
|
|
106
|
+
/** tool spans by `<issuing turn spanId>:<callId>` — a reused id is a new span in a new turn */
|
|
107
|
+
tools: Map<string, OtelSpan>;
|
|
108
|
+
/** issued calls: dispatched (spans) + never-dispatched (tool_call_failed events) */
|
|
109
|
+
calls: number;
|
|
110
|
+
/** store length at pre_run (run totals) and at the last turn_end (per-turn usage) */
|
|
111
|
+
baseline: number; seen: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const OK: OtelSpan["status"] = { code: 1 };
|
|
115
|
+
const ERROR = (message: string): OtelSpan["status"] => ({ code: 2, message });
|
|
116
|
+
|
|
117
|
+
// ---------- env / endpoint / headers ----------
|
|
118
|
+
|
|
119
|
+
/** null unless ROVECODE_OTEL_ENDPOINT is set (blank = unset) — the runtime constructs nothing on null */
|
|
120
|
+
export function otelOptionsFromEnv(env: Record<string, string | undefined> = process.env): Pick<OtelOptions, "endpoint" | "headers"> | null {
|
|
121
|
+
const endpoint = (env["ROVECODE_OTEL_ENDPOINT"] ?? "").trim();
|
|
122
|
+
return endpoint ? { endpoint, headers: parseOtelHeaders(env["ROVECODE_OTEL_HEADERS"]) } : null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** "k=v,k2=v2" (OTEL_EXPORTER_OTLP_HEADERS shape): first "=" splits, so values may contain "=";
|
|
126
|
+
* blank or key-less entries are skipped; values are taken raw (no percent-decoding). */
|
|
127
|
+
export function parseOtelHeaders(text: string | undefined): Record<string, string> {
|
|
128
|
+
const out: Record<string, string> = {};
|
|
129
|
+
for (const part of (text ?? "").split(",")) {
|
|
130
|
+
const eq = part.indexOf("=");
|
|
131
|
+
if (eq <= 0) continue;
|
|
132
|
+
const key = part.slice(0, eq).trim();
|
|
133
|
+
if (key) out[key] = part.slice(eq + 1).trim();
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** base URL or full traces URL (either, with or without trailing slashes) → <base>/v1/traces */
|
|
139
|
+
export function normalizeEndpoint(endpoint: string): string {
|
|
140
|
+
const base = endpoint.trim().replace(/\/+$/, "");
|
|
141
|
+
return base.endsWith(OTLP_TRACES_PATH) ? base : base + OTLP_TRACES_PATH;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** an absolute http(s) URL with a host — `http://` does not parse and `host:4318` parses host-less,
|
|
145
|
+
* and both would normalize to `http:/v1/traces` (host "v1") and stall every export to the timeout */
|
|
146
|
+
export function validEndpoint(endpoint: string): boolean {
|
|
147
|
+
try { const u = new URL(endpoint.trim()); return (u.protocol === "http:" || u.protocol === "https:") && u.hostname !== ""; } catch { return false; }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---------- the hook set ----------
|
|
151
|
+
|
|
152
|
+
export function createOtelHooks(opts: OtelOptions): OtelHooks {
|
|
153
|
+
otelDebug.constructed++;
|
|
154
|
+
const url = normalizeEndpoint(opts.endpoint);
|
|
155
|
+
const headers = { ...(opts.headers ?? {}), "content-type": "application/json" };
|
|
156
|
+
const fetchFn = opts.fetch ?? fetch;
|
|
157
|
+
const now = opts.now ?? defaultNow;
|
|
158
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_EXPORT_TIMEOUT_MS;
|
|
159
|
+
const service = opts.serviceName ?? "rovecode";
|
|
160
|
+
let pricing: PricingSource | undefined = opts.pricing;
|
|
161
|
+
const runs = new Map<string, RunState>();
|
|
162
|
+
const pending = new Set<Promise<void>>();
|
|
163
|
+
let failed = 0, lastFailure = "";
|
|
164
|
+
|
|
165
|
+
const stateOf = (ctx: HookCtx): RunState | undefined => (ctx.runId === undefined ? undefined : runs.get(ctx.runId));
|
|
166
|
+
const attach = (st: RunState, name: string, parent: OtelSpan): OtelSpan => {
|
|
167
|
+
const s: OtelSpan = { traceId: st.run.traceId, spanId: hex(8), parentSpanId: parent.spanId, name, start: now(), attrs: new Map(), events: [], status: { code: 0 } };
|
|
168
|
+
st.spans.push(s);
|
|
169
|
+
return s;
|
|
170
|
+
};
|
|
171
|
+
const end = (s: OtelSpan, status: OtelSpan["status"]): void => { if (s.end === undefined) { s.end = now(); s.status = status; } };
|
|
172
|
+
const event = (s: OtelSpan, name: string, attrs: [string, OtlpValue][]): void => { s.events.push({ name, time: now(), attrs: new Map(attrs) }); };
|
|
173
|
+
/** a call's parent = the turn that issued it (turn_end precedes the turn's tool events — header) */
|
|
174
|
+
const issuer = (st: RunState): OtelSpan => st.lastTurn ?? st.run;
|
|
175
|
+
const toolKey = (st: RunState, callId: string): string => `${issuer(st).spanId}:${callId}`;
|
|
176
|
+
/** the tool span for a call — opened by whichever of pre_tool / tool_execution_start arrives first;
|
|
177
|
+
* a span the START event has to open never saw pre_tool: the loop-guard stub path (tools.ts:88-93) */
|
|
178
|
+
const openTool = (st: RunState, callId: string, tool?: string, fromStart = false): OtelSpan => {
|
|
179
|
+
const key = toolKey(st, callId);
|
|
180
|
+
let s = st.tools.get(key);
|
|
181
|
+
if (!s) {
|
|
182
|
+
s = attach(st, "rovecode.tool", issuer(st));
|
|
183
|
+
s.attrs.set("rovecode.call_id", str(callId));
|
|
184
|
+
if (fromStart) s.attrs.set("rovecode.failure_reason", str("loop_guard"));
|
|
185
|
+
st.tools.set(key, s);
|
|
186
|
+
st.calls++;
|
|
187
|
+
}
|
|
188
|
+
if (tool !== undefined && !s.attrs.has("rovecode.tool")) s.attrs.set("rovecode.tool", str(tool));
|
|
189
|
+
return s;
|
|
190
|
+
};
|
|
191
|
+
const settleTool = (s: OtelSpan, ok: boolean, output: string): void => {
|
|
192
|
+
if (s.end !== undefined) return; // post_tool already settled it; tool_execution_end only adds duration
|
|
193
|
+
s.attrs.set("rovecode.ok", bool(ok));
|
|
194
|
+
s.attrs.set("rovecode.output_bytes", int(Buffer.byteLength(output, "utf8")));
|
|
195
|
+
const name = s.attrs.get("rovecode.tool");
|
|
196
|
+
end(s, ok ? OK : ERROR(`tool ${name && "stringValue" in name ? name.stringValue : "call"} failed`));
|
|
197
|
+
};
|
|
198
|
+
/** tokens summed + cost priced PER MESSAGE at Message.origin (cli/output.ts summarize): any
|
|
199
|
+
* usage-bearing message without catalog pricing → cost unknown → attribute omitted, never 0 */
|
|
200
|
+
const account = (s: OtelSpan, msgs: Message[]): void => {
|
|
201
|
+
const u = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
202
|
+
let cost: number | null = 0;
|
|
203
|
+
for (const m of msgs) {
|
|
204
|
+
if (!m.usage) continue;
|
|
205
|
+
const n = { input: m.usage.input, output: m.usage.output, cacheRead: m.usage.cacheRead ?? 0, cacheWrite: m.usage.cacheWrite ?? 0 };
|
|
206
|
+
u.input += n.input; u.output += n.output; u.cacheRead += n.cacheRead; u.cacheWrite += n.cacheWrite;
|
|
207
|
+
if (cost === null || (n.input === 0 && n.output === 0 && n.cacheRead === 0 && n.cacheWrite === 0)) continue;
|
|
208
|
+
const row = m.origin ? (pricing ??= new ModelCatalog()).lookup(m.origin.provider, m.origin.model)?.pricing : undefined;
|
|
209
|
+
const c = row ? costUsd(n, row) : undefined;
|
|
210
|
+
cost = c === undefined ? null : cost + c;
|
|
211
|
+
}
|
|
212
|
+
s.attrs.set("rovecode.tokens.input", int(u.input)); s.attrs.set("rovecode.tokens.output", int(u.output));
|
|
213
|
+
s.attrs.set("rovecode.tokens.cacheRead", int(u.cacheRead)); s.attrs.set("rovecode.tokens.cacheWrite", int(u.cacheWrite));
|
|
214
|
+
if (cost !== null) s.attrs.set("rovecode.cost_usd", dbl(cost));
|
|
215
|
+
const served = msgs.at(-1)?.origin; // the model that SERVED (router fallback may differ from the request)
|
|
216
|
+
if (served) { s.attrs.set("rovecode.model.provider", str(served.provider)); s.attrs.set("rovecode.model.model", str(served.model)); }
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// --- export ---
|
|
220
|
+
const warn = (note: string): void => {
|
|
221
|
+
if (opts.onWarning) { opts.onWarning(note); return; }
|
|
222
|
+
failed++; lastFailure = note;
|
|
223
|
+
};
|
|
224
|
+
const fail = (reason: string): void => warn(`OTLP export to ${url} failed: ${reason}`);
|
|
225
|
+
const usable = validEndpoint(opts.endpoint); // ONE note at construction (raised by the first lifecycle hook), then no POSTs
|
|
226
|
+
if (!usable) warn(`ROVECODE_OTEL_ENDPOINT ${JSON.stringify(opts.endpoint)} is not an absolute http(s) URL — OTel export disabled`);
|
|
227
|
+
/** deferred failure → thrown from a lifecycle hook → one runner warning (header); no-op with onWarning */
|
|
228
|
+
const raise = (): void => {
|
|
229
|
+
if (failed === 0) return;
|
|
230
|
+
const n = failed; failed = 0;
|
|
231
|
+
throw new Error(n === 1 ? lastFailure : `${lastFailure} (${n} exports failed)`);
|
|
232
|
+
};
|
|
233
|
+
const post = async (body: string): Promise<void> => {
|
|
234
|
+
if (!usable) return;
|
|
235
|
+
const ac = new AbortController();
|
|
236
|
+
try {
|
|
237
|
+
const res = await withTimeout(fetchFn(url, { method: "POST", headers, body, signal: ac.signal }), timeoutMs, ac);
|
|
238
|
+
if (res === TIMED_OUT) { fail(`timed out after ${timeoutMs}ms`); return; }
|
|
239
|
+
await res.arrayBuffer().catch(() => undefined); // release the connection; a success body is `{}`
|
|
240
|
+
if (!res.ok) fail(`HTTP ${res.status}`);
|
|
241
|
+
} catch (e) { fail(errText(e)); }
|
|
242
|
+
};
|
|
243
|
+
const track = (p: Promise<void>): void => { pending.add(p); void p.then(() => { pending.delete(p); }); };
|
|
244
|
+
const flush = async (): Promise<void> => { while (pending.size > 0) await Promise.all([...pending]); };
|
|
245
|
+
/** the run's end: open turn/tool spans close here (status UNSET), totals + status land on the run
|
|
246
|
+
* span, ONE POST — from post_run, or from session_close for a state no post_run ever released */
|
|
247
|
+
const finish = (st: RunState, status: RunResult["status"]): void => {
|
|
248
|
+
const t = now();
|
|
249
|
+
for (const s of st.spans) if (s.end === undefined && s !== st.run) s.end = t;
|
|
250
|
+
st.run.attrs.set("rovecode.status", str(status));
|
|
251
|
+
st.run.attrs.set("rovecode.turns", int(st.spans.filter((s) => s.name === "rovecode.turn").length));
|
|
252
|
+
st.run.attrs.set("rovecode.tool_calls", int(st.calls));
|
|
253
|
+
account(st.run, opts.messages().slice(st.baseline).filter((m) => m.role === "assistant"));
|
|
254
|
+
st.run.end = t;
|
|
255
|
+
st.run.status = status === "done" || status === "stopped" ? OK : ERROR(status); // budget/error did not complete
|
|
256
|
+
track(post(JSON.stringify(encodeTraceRequest(service, st.spans))));
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
flush,
|
|
261
|
+
openRuns: () => runs.size,
|
|
262
|
+
pre_run(ctx) {
|
|
263
|
+
if (ctx.runId === undefined) return;
|
|
264
|
+
const run: OtelSpan = { traceId: hex(16), spanId: hex(8), name: "rovecode.run", start: now(), attrs: new Map(), events: [], status: { code: 0 } };
|
|
265
|
+
run.attrs.set("rovecode.session_id", str(ctx.sessionId)); run.attrs.set("rovecode.run_id", str(ctx.runId));
|
|
266
|
+
const len = opts.messages().length;
|
|
267
|
+
runs.set(ctx.runId, { run, spans: [run], tools: new Map(), calls: 0, baseline: len, seen: len });
|
|
268
|
+
raise();
|
|
269
|
+
},
|
|
270
|
+
on_event(ctx, ev: RunEvent) {
|
|
271
|
+
const st = stateOf(ctx);
|
|
272
|
+
if (!st) return;
|
|
273
|
+
switch (ev.type) {
|
|
274
|
+
case "turn_start": {
|
|
275
|
+
if (st.turn) end(st.turn, st.turn.status); // defensive: the loop never nests turns
|
|
276
|
+
st.turn = st.lastTurn = attach(st, "rovecode.turn", st.run);
|
|
277
|
+
st.turn.attrs.set("rovecode.turn", int(ev.turn));
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
case "turn_end": {
|
|
281
|
+
const t = st.turn ?? st.lastTurn;
|
|
282
|
+
if (!t) break;
|
|
283
|
+
t.attrs.set("rovecode.stop_reason", str(ev.stopReason));
|
|
284
|
+
const msgs = opts.messages();
|
|
285
|
+
account(t, msgs.slice(st.seen).filter((m) => m.role === "assistant"));
|
|
286
|
+
st.seen = msgs.length;
|
|
287
|
+
end(t, ev.stopReason === "error" ? ERROR("error") : OK);
|
|
288
|
+
st.turn = undefined;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
case "tool_execution_start": openTool(st, ev.callId, ev.tool, true); break;
|
|
292
|
+
case "tool_execution_end": {
|
|
293
|
+
const s = openTool(st, ev.callId);
|
|
294
|
+
s.attrs.set("rovecode.duration_ms", int(ev.durationMs)); // the dispatcher's own measure (tools.ts:170)
|
|
295
|
+
settleTool(s, ev.ok, ev.output);
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
case "tool_call_failed": {
|
|
299
|
+
const s = st.tools.get(toolKey(st, ev.callId));
|
|
300
|
+
if (s) { s.attrs.set("rovecode.ok", bool(false)); s.attrs.set("rovecode.failure_reason", str(ev.reason)); end(s, ERROR(ev.reason)); }
|
|
301
|
+
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)]]); }
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
case "compaction":
|
|
305
|
+
event(st.turn ?? st.run, "rovecode.compaction", [
|
|
306
|
+
["rovecode.compaction.strategy", str(ev.strategy)], ...(ev.trigger ? [["rovecode.compaction.trigger", str(ev.trigger)] as [string, OtlpValue]] : []),
|
|
307
|
+
["rovecode.compaction.tokens_before", int(ev.tokensBefore)], ["rovecode.compaction.tokens_after", int(ev.tokensAfter)],
|
|
308
|
+
]);
|
|
309
|
+
break;
|
|
310
|
+
default: break; // run_start/run_end ride pre_run/post_run; deltas, progress notes and steers are not spans
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
pre_tool(ctx, call: HookToolCall) { const st = stateOf(ctx); if (st) openTool(st, call.id, call.tool); },
|
|
314
|
+
post_tool(ctx, call: HookToolCall, result: ToolOutput) { const st = stateOf(ctx); if (st) settleTool(openTool(st, call.id, call.tool), result.ok, result.output); },
|
|
315
|
+
post_run(ctx, result: RunResult) {
|
|
316
|
+
const st = stateOf(ctx);
|
|
317
|
+
if (!st) return;
|
|
318
|
+
runs.delete(ctx.runId!);
|
|
319
|
+
finish(st, result.status);
|
|
320
|
+
},
|
|
321
|
+
async session_close() {
|
|
322
|
+
// belt and braces (#39 MED-1): a run whose generator was dropped without .return() never reached
|
|
323
|
+
// post_run — export what it recorded as "stopped" (open spans close UNSET) instead of leaking it
|
|
324
|
+
for (const [id, st] of runs) { runs.delete(id); finish(st, "stopped"); }
|
|
325
|
+
await flush(); raise();
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ---------- helpers ----------
|
|
331
|
+
|
|
332
|
+
const defaultNow = (): number => performance.timeOrigin + performance.now();
|
|
333
|
+
/** random hex id of `bytes` bytes; the all-zero id is invalid in OTel */
|
|
334
|
+
function hex(bytes: number): string {
|
|
335
|
+
let h: string;
|
|
336
|
+
do h = randomBytes(bytes).toString("hex"); while (/^0+$/.test(h));
|
|
337
|
+
return h;
|
|
338
|
+
}
|
|
339
|
+
const TIMED_OUT: unique symbol = Symbol("rovecode.otel.timeout");
|
|
340
|
+
/** resolves TIMED_OUT after ms on a REF'D timer and aborts the request's controller (hooks.ts idiom);
|
|
341
|
+
* the race is independent of the fetch honoring the signal, so a stuck transport still settles */
|
|
342
|
+
function withTimeout<T>(p: Promise<T>, ms: number, ac: AbortController): Promise<T | typeof TIMED_OUT> {
|
|
343
|
+
return new Promise((resolve, reject) => {
|
|
344
|
+
const timer = setTimeout(() => { ac.abort(); resolve(TIMED_OUT); }, ms);
|
|
345
|
+
(timer as unknown as { ref?: () => void }).ref?.();
|
|
346
|
+
p.then((v) => { clearTimeout(timer); resolve(v); }, (e: unknown) => { clearTimeout(timer); reject(e); });
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
function errText(e: unknown): string {
|
|
350
|
+
if (e instanceof Error) return e.message;
|
|
351
|
+
if (typeof e === "object" && e !== null && typeof (e as { message?: unknown }).message === "string") return (e as { message: string }).message;
|
|
352
|
+
return String(e);
|
|
353
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
|
|
7
|
+
import pkg from "../../package.json";
|
|
8
|
+
|
|
9
|
+
// ---------- wire types (OTLP/HTTP JSON, ExportTraceServiceRequest) ----------
|
|
10
|
+
|
|
11
|
+
export type OtlpValue = { stringValue: string } | { intValue: string } | { doubleValue: number } | { boolValue: boolean };
|
|
12
|
+
export interface OtlpKeyValue { key: string; value: OtlpValue }
|
|
13
|
+
export interface OtlpSpan {
|
|
14
|
+
traceId: string; spanId: string; parentSpanId?: string; name: string; kind: number;
|
|
15
|
+
startTimeUnixNano: string; endTimeUnixNano: string;
|
|
16
|
+
attributes: OtlpKeyValue[];
|
|
17
|
+
events?: { name: string; timeUnixNano: string; attributes: OtlpKeyValue[] }[];
|
|
18
|
+
status: { code: number; message?: string };
|
|
19
|
+
}
|
|
20
|
+
export interface OtlpTraceRequest {
|
|
21
|
+
resourceSpans: {
|
|
22
|
+
resource: { attributes: OtlpKeyValue[] };
|
|
23
|
+
scopeSpans: { scope: { name: string; version?: string }; spans: OtlpSpan[] }[];
|
|
24
|
+
}[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** one recorded span; mutable until its run is exported (events arrive out of order — otel.ts header) */
|
|
28
|
+
export interface OtelSpan {
|
|
29
|
+
traceId: string; spanId: string; parentSpanId?: string; name: string;
|
|
30
|
+
start: number; end?: number;
|
|
31
|
+
attrs: Map<string, OtlpValue>;
|
|
32
|
+
events: { name: string; time: number; attrs: Map<string, OtlpValue> }[];
|
|
33
|
+
status: { code: 0 | 1 | 2; message?: string };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const str = (v: string): OtlpValue => ({ stringValue: v });
|
|
37
|
+
export const int = (v: number): OtlpValue => ({ intValue: String(Math.trunc(v)) });
|
|
38
|
+
export const dbl = (v: number): OtlpValue => ({ doubleValue: v });
|
|
39
|
+
export const bool = (v: boolean): OtlpValue => ({ boolValue: v });
|
|
40
|
+
|
|
41
|
+
// ---------- encoding ----------
|
|
42
|
+
|
|
43
|
+
/** OTLP/JSON: ids hex (already), fixed64 times + int64 attrs as decimal strings, enums as integers */
|
|
44
|
+
export function encodeTraceRequest(serviceName: string, spans: readonly OtelSpan[]): OtlpTraceRequest {
|
|
45
|
+
const kv = (m: Iterable<[string, OtlpValue]>): OtlpKeyValue[] => [...m].map(([key, value]) => ({ key, value }));
|
|
46
|
+
return {
|
|
47
|
+
resourceSpans: [{
|
|
48
|
+
resource: { attributes: kv([["service.name", str(serviceName)], ["service.version", str(pkg.version)]]) },
|
|
49
|
+
scopeSpans: [{
|
|
50
|
+
scope: { name: "rovecode", version: pkg.version },
|
|
51
|
+
spans: spans.map((s) => ({
|
|
52
|
+
traceId: s.traceId, spanId: s.spanId, ...(s.parentSpanId ? { parentSpanId: s.parentSpanId } : {}),
|
|
53
|
+
name: s.name, kind: 1, // SPAN_KIND_INTERNAL
|
|
54
|
+
startTimeUnixNano: unixNano(s.start), endTimeUnixNano: unixNano(s.end ?? s.start),
|
|
55
|
+
attributes: kv(s.attrs),
|
|
56
|
+
...(s.events.length > 0 ? { events: s.events.map((e) => ({ name: e.name, timeUnixNano: unixNano(e.time), attributes: kv(e.attrs) })) } : {}),
|
|
57
|
+
status: s.status.message !== undefined ? { code: s.status.code, message: s.status.message } : { code: s.status.code },
|
|
58
|
+
})),
|
|
59
|
+
}],
|
|
60
|
+
}],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** ms since epoch (fractions allowed) → exact unix-nano decimal string (no float rounding at 1e18) */
|
|
65
|
+
export function unixNano(ms: number): string {
|
|
66
|
+
const whole = Math.floor(ms);
|
|
67
|
+
return (BigInt(whole) * 1_000_000n + BigInt(Math.round((ms - whole) * 1e6))).toString();
|
|
68
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
}
|