gsd-pi 2.63.0 → 2.64.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 +46 -134
- package/dist/cli.js +48 -6
- package/dist/headless-query.js +11 -1
- package/dist/help-text.js +4 -1
- package/dist/onboarding.js +15 -8
- package/dist/resource-loader.js +18 -3
- package/dist/resources/extensions/cmux/index.js +21 -12
- package/dist/resources/extensions/gsd/auto/detect-stuck.js +27 -0
- package/dist/resources/extensions/gsd/auto/finalize-timeout.js +40 -0
- package/dist/resources/extensions/gsd/auto/loop.js +4 -0
- package/dist/resources/extensions/gsd/auto/phases.js +157 -22
- package/dist/resources/extensions/gsd/auto/session.js +12 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +9 -3
- package/dist/resources/extensions/gsd/auto-model-selection.js +32 -0
- package/dist/resources/extensions/gsd/auto-post-unit.js +124 -10
- package/dist/resources/extensions/gsd/auto-prompts.js +25 -0
- package/dist/resources/extensions/gsd/auto-recovery.js +15 -7
- package/dist/resources/extensions/gsd/auto-start.js +10 -21
- package/dist/resources/extensions/gsd/auto-timers.js +2 -1
- package/dist/resources/extensions/gsd/auto-tool-tracking.js +17 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +13 -7
- package/dist/resources/extensions/gsd/auto.js +19 -2
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +147 -75
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +13 -0
- package/dist/resources/extensions/gsd/bootstrap/query-tools.js +85 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +3 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +32 -1
- package/dist/resources/extensions/gsd/bootstrap/sanitize-complete-milestone.js +54 -0
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +30 -2
- package/dist/resources/extensions/gsd/commands-handlers.js +9 -4
- package/dist/resources/extensions/gsd/constants.js +42 -0
- package/dist/resources/extensions/gsd/db-writer.js +72 -4
- package/dist/resources/extensions/gsd/forensics.js +20 -4
- package/dist/resources/extensions/gsd/gsd-db.js +64 -17
- package/dist/resources/extensions/gsd/guided-flow.js +19 -0
- package/dist/resources/extensions/gsd/metrics.js +27 -1
- package/dist/resources/extensions/gsd/native-git-bridge.js +5 -3
- package/dist/resources/extensions/gsd/preferences-types.js +2 -0
- package/dist/resources/extensions/gsd/preferences.js +7 -2
- package/dist/resources/extensions/gsd/prompt-loader.js +7 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +2 -0
- package/dist/resources/extensions/gsd/prompts/doctor-heal.md +1 -0
- package/dist/resources/extensions/gsd/prompts/forensics.md +2 -0
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +2 -0
- package/dist/resources/extensions/gsd/prompts/system.md +4 -7
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
- package/dist/resources/extensions/gsd/roadmap-mutations.js +1 -1
- package/dist/resources/extensions/gsd/roadmap-slices.js +9 -5
- package/dist/resources/extensions/gsd/safety/content-validator.js +73 -0
- package/dist/resources/extensions/gsd/safety/destructive-guard.js +34 -0
- package/dist/resources/extensions/gsd/safety/evidence-collector.js +109 -0
- package/dist/resources/extensions/gsd/safety/evidence-cross-ref.js +83 -0
- package/dist/resources/extensions/gsd/safety/file-change-validator.js +71 -0
- package/dist/resources/extensions/gsd/safety/git-checkpoint.js +91 -0
- package/dist/resources/extensions/gsd/safety/safety-harness.js +64 -0
- package/dist/resources/extensions/gsd/slice-parallel-conflict.js +67 -0
- package/dist/resources/extensions/gsd/slice-parallel-eligibility.js +51 -0
- package/dist/resources/extensions/gsd/slice-parallel-orchestrator.js +378 -0
- package/dist/resources/extensions/gsd/state.js +74 -14
- package/dist/resources/extensions/gsd/status-guards.js +11 -0
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +17 -12
- package/dist/resources/extensions/gsd/tools/complete-slice.js +40 -26
- package/dist/resources/extensions/gsd/tools/complete-task.js +12 -12
- package/dist/resources/extensions/gsd/tools/plan-milestone.js +33 -25
- package/dist/resources/extensions/gsd/tools/plan-slice.js +5 -8
- package/dist/resources/extensions/gsd/workflow-projections.js +21 -5
- package/dist/resources/extensions/gsd/worktree-manager.js +82 -29
- package/dist/resources/extensions/gsd/worktree-resolver.js +4 -3
- package/dist/resources/extensions/mcp-client/auth.js +101 -0
- package/dist/resources/extensions/mcp-client/index.js +10 -1
- package/dist/resources/extensions/ollama/index.js +28 -22
- package/dist/resources/extensions/ollama/model-capabilities.js +37 -34
- package/dist/resources/extensions/ollama/ndjson-stream.js +54 -0
- package/dist/resources/extensions/ollama/ollama-chat-provider.js +380 -0
- package/dist/resources/extensions/ollama/ollama-client.js +23 -32
- package/dist/resources/extensions/ollama/ollama-discovery.js +2 -7
- package/dist/resources/extensions/ollama/ollama-tool.js +62 -0
- package/dist/resources/extensions/ollama/thinking-parser.js +104 -0
- package/dist/update-cmd.js +4 -2
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
- package/dist/web/standalone/.next/server/chunks/6897.js +12 -0
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/welcome-screen.js +1 -1
- package/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.d.ts +8 -0
- package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +50 -0
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.test.ts +221 -5
- package/packages/pi-agent-core/src/agent-loop.ts +53 -0
- package/packages/pi-ai/dist/types.d.ts +16 -1
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/src/types.ts +18 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js +50 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js +41 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts +7 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +31 -4
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.test.js +28 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/provider-registration.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/provider-registration.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/provider-registration.test.js +46 -0
- package/packages/pi-coding-agent/dist/core/extensions/provider-registration.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +12 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +3 -3
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts +23 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.js +80 -56
- package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +9 -0
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/auth-storage.test.ts +53 -0
- package/packages/pi-coding-agent/src/core/auth-storage.ts +66 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.test.ts +39 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +34 -4
- package/packages/pi-coding-agent/src/core/extensions/provider-registration.test.ts +81 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +14 -0
- package/packages/pi-coding-agent/src/core/model-resolver.ts +3 -3
- package/packages/pi-coding-agent/src/core/resource-loader.ts +89 -56
- package/packages/pi-coding-agent/src/core/sdk.ts +10 -0
- package/pkg/package.json +1 -1
- package/src/resources/extensions/cmux/index.ts +18 -12
- package/src/resources/extensions/gsd/auto/detect-stuck.ts +27 -0
- package/src/resources/extensions/gsd/auto/finalize-timeout.ts +46 -0
- package/src/resources/extensions/gsd/auto/loop.ts +5 -0
- package/src/resources/extensions/gsd/auto/phases.ts +194 -33
- package/src/resources/extensions/gsd/auto/session.ts +14 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +11 -3
- package/src/resources/extensions/gsd/auto-model-selection.ts +36 -0
- package/src/resources/extensions/gsd/auto-post-unit.ts +141 -12
- package/src/resources/extensions/gsd/auto-prompts.ts +21 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +9 -8
- package/src/resources/extensions/gsd/auto-start.ts +11 -20
- package/src/resources/extensions/gsd/auto-timers.ts +2 -1
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +19 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +14 -6
- package/src/resources/extensions/gsd/auto.ts +22 -1
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +160 -88
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +15 -0
- package/src/resources/extensions/gsd/bootstrap/query-tools.ts +98 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +4 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +36 -1
- package/src/resources/extensions/gsd/bootstrap/sanitize-complete-milestone.ts +57 -0
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +31 -2
- package/src/resources/extensions/gsd/commands-handlers.ts +10 -4
- package/src/resources/extensions/gsd/constants.ts +44 -0
- package/src/resources/extensions/gsd/db-writer.ts +78 -4
- package/src/resources/extensions/gsd/forensics.ts +21 -5
- package/src/resources/extensions/gsd/gsd-db.ts +64 -17
- package/src/resources/extensions/gsd/guided-flow.ts +22 -0
- package/src/resources/extensions/gsd/metrics.ts +28 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +5 -3
- package/src/resources/extensions/gsd/preferences-types.ts +16 -0
- package/src/resources/extensions/gsd/preferences.ts +9 -2
- package/src/resources/extensions/gsd/prompt-loader.ts +8 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +2 -0
- package/src/resources/extensions/gsd/prompts/doctor-heal.md +1 -0
- package/src/resources/extensions/gsd/prompts/forensics.md +2 -0
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +2 -0
- package/src/resources/extensions/gsd/prompts/system.md +4 -7
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
- package/src/resources/extensions/gsd/roadmap-mutations.ts +1 -1
- package/src/resources/extensions/gsd/roadmap-slices.ts +10 -5
- package/src/resources/extensions/gsd/safety/content-validator.ts +98 -0
- package/src/resources/extensions/gsd/safety/destructive-guard.ts +49 -0
- package/src/resources/extensions/gsd/safety/evidence-collector.ts +151 -0
- package/src/resources/extensions/gsd/safety/evidence-cross-ref.ts +120 -0
- package/src/resources/extensions/gsd/safety/file-change-validator.ts +108 -0
- package/src/resources/extensions/gsd/safety/git-checkpoint.ts +106 -0
- package/src/resources/extensions/gsd/safety/safety-harness.ts +105 -0
- package/src/resources/extensions/gsd/slice-parallel-conflict.ts +86 -0
- package/src/resources/extensions/gsd/slice-parallel-eligibility.ts +73 -0
- package/src/resources/extensions/gsd/slice-parallel-orchestrator.ts +477 -0
- package/src/resources/extensions/gsd/state.ts +67 -12
- package/src/resources/extensions/gsd/status-guards.ts +13 -0
- package/src/resources/extensions/gsd/tests/artifact-corruption-2630.test.ts +288 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +34 -13
- package/src/resources/extensions/gsd/tests/cmux.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/cold-resume-db-reopen.test.ts +51 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +140 -0
- package/src/resources/extensions/gsd/tests/complete-slice-string-coercion.test.ts +211 -0
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/dashboard-model-label-ordering.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/db-access-guardrails.test.ts +109 -0
- package/src/resources/extensions/gsd/tests/db-path-worktree-symlink.test.ts +13 -9
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +134 -0
- package/src/resources/extensions/gsd/tests/deferred-slice-dispatch.test.ts +203 -0
- package/src/resources/extensions/gsd/tests/discuss-tool-scoping.test.ts +130 -0
- package/src/resources/extensions/gsd/tests/doctor-fix-flag.test.ts +92 -0
- package/src/resources/extensions/gsd/tests/finalize-timeout-guard.test.ts +116 -0
- package/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/forensics-stuck-loops.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/git-checkpoint.test.ts +94 -0
- package/src/resources/extensions/gsd/tests/insert-slice-no-wipe.test.ts +88 -0
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +27 -7
- package/src/resources/extensions/gsd/tests/integration/idle-recovery.test.ts +34 -0
- package/src/resources/extensions/gsd/tests/metrics.test.ts +116 -1
- package/src/resources/extensions/gsd/tests/milestone-status-tool.test.ts +201 -0
- package/src/resources/extensions/gsd/tests/plan-milestone-title.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +82 -18
- package/src/resources/extensions/gsd/tests/preferences.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +69 -0
- package/src/resources/extensions/gsd/tests/shared-wal.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/slice-context-injection.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/slice-parallel-conflict.test.ts +92 -0
- package/src/resources/extensions/gsd/tests/slice-parallel-eligibility.test.ts +95 -0
- package/src/resources/extensions/gsd/tests/slice-parallel-orchestrator.test.ts +83 -0
- package/src/resources/extensions/gsd/tests/stuck-detection-coverage.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +349 -0
- package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +35 -2
- package/src/resources/extensions/gsd/tests/worktree-health-monorepo.test.ts +73 -0
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +34 -0
- package/src/resources/extensions/gsd/tests/worktree-submodule-safety.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-teardown-safety.test.ts +148 -0
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +34 -20
- package/src/resources/extensions/gsd/tools/complete-slice.ts +41 -26
- package/src/resources/extensions/gsd/tools/complete-task.ts +12 -12
- package/src/resources/extensions/gsd/tools/plan-milestone.ts +55 -30
- package/src/resources/extensions/gsd/tools/plan-slice.ts +13 -8
- package/src/resources/extensions/gsd/types.ts +44 -22
- package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
- package/src/resources/extensions/gsd/workflow-projections.ts +23 -5
- package/src/resources/extensions/gsd/worktree-manager.ts +76 -28
- package/src/resources/extensions/gsd/worktree-resolver.ts +4 -3
- package/src/resources/extensions/mcp-client/auth.ts +149 -0
- package/src/resources/extensions/mcp-client/index.ts +16 -1
- package/src/resources/extensions/ollama/index.ts +26 -25
- package/src/resources/extensions/ollama/model-capabilities.ts +41 -34
- package/src/resources/extensions/ollama/ndjson-stream.ts +63 -0
- package/src/resources/extensions/ollama/ollama-auth-mode.test.ts +20 -0
- package/src/resources/extensions/ollama/ollama-chat-provider.ts +459 -0
- package/src/resources/extensions/ollama/ollama-client.ts +30 -30
- package/src/resources/extensions/ollama/ollama-discovery.ts +5 -8
- package/src/resources/extensions/ollama/ollama-tool.ts +69 -0
- package/src/resources/extensions/ollama/tests/ollama-chat-provider-stream.test.ts +82 -0
- package/src/resources/extensions/ollama/tests/ollama-discovery.test.ts +0 -27
- package/src/resources/extensions/ollama/thinking-parser.ts +116 -0
- package/src/resources/extensions/ollama/types.ts +23 -0
- package/dist/web/standalone/.next/server/chunks/2229.js +0 -12
- /package/dist/web/standalone/.next/static/{5FLUBNdqolRyyehCyChPd → eebXKteM9EaWyseHKTjqp}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{5FLUBNdqolRyyehCyChPd → eebXKteM9EaWyseHKTjqp}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
// GSD2 — Ollama Extension: Native /api/chat stream provider
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Implements the "ollama-chat" API provider, streaming responses directly
|
|
5
|
+
* from Ollama's native /api/chat endpoint instead of the OpenAI compatibility
|
|
6
|
+
* shim. This exposes Ollama-specific options (num_ctx, keep_alive, num_gpu,
|
|
7
|
+
* sampling parameters) and surfaces inference performance metrics.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
type Api,
|
|
12
|
+
type AssistantMessage,
|
|
13
|
+
type AssistantMessageEvent,
|
|
14
|
+
type AssistantMessageEventStream,
|
|
15
|
+
type Context,
|
|
16
|
+
type ImageContent,
|
|
17
|
+
type InferenceMetrics,
|
|
18
|
+
type Message,
|
|
19
|
+
type Model,
|
|
20
|
+
type SimpleStreamOptions,
|
|
21
|
+
type StopReason,
|
|
22
|
+
type TextContent,
|
|
23
|
+
type ThinkingContent,
|
|
24
|
+
type Tool,
|
|
25
|
+
type ToolCall,
|
|
26
|
+
type Usage,
|
|
27
|
+
EventStream,
|
|
28
|
+
} from "@gsd/pi-ai";
|
|
29
|
+
import { chat } from "./ollama-client.js";
|
|
30
|
+
import type {
|
|
31
|
+
OllamaChatMessage,
|
|
32
|
+
OllamaChatOptions,
|
|
33
|
+
OllamaChatRequest,
|
|
34
|
+
OllamaChatResponse,
|
|
35
|
+
OllamaTool,
|
|
36
|
+
OllamaToolCall,
|
|
37
|
+
} from "./types.js";
|
|
38
|
+
import { ThinkingTagParser, type ParsedChunk } from "./thinking-parser.js";
|
|
39
|
+
|
|
40
|
+
/** Create an AssistantMessageEventStream using the base EventStream class. */
|
|
41
|
+
function createStream(): AssistantMessageEventStream {
|
|
42
|
+
return new EventStream<AssistantMessageEvent, AssistantMessage>(
|
|
43
|
+
(event) => event.type === "done" || event.type === "error",
|
|
44
|
+
(event) => {
|
|
45
|
+
if (event.type === "done") return event.message;
|
|
46
|
+
if (event.type === "error") return event.error;
|
|
47
|
+
throw new Error("Unexpected event type for final result");
|
|
48
|
+
},
|
|
49
|
+
) as AssistantMessageEventStream;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Stream handler ─────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
export function streamOllamaChat(
|
|
55
|
+
model: Model<Api>,
|
|
56
|
+
context: Context,
|
|
57
|
+
options?: SimpleStreamOptions,
|
|
58
|
+
): AssistantMessageEventStream {
|
|
59
|
+
const stream = createStream();
|
|
60
|
+
|
|
61
|
+
(async () => {
|
|
62
|
+
const output = buildInitialOutput(model);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const request = buildRequest(model, context, options);
|
|
66
|
+
stream.push({ type: "start", partial: output });
|
|
67
|
+
|
|
68
|
+
const useThinkingParser = model.reasoning;
|
|
69
|
+
const thinkParser = useThinkingParser ? new ThinkingTagParser() : null;
|
|
70
|
+
|
|
71
|
+
let contentIndex = -1;
|
|
72
|
+
let currentBlockType: "text" | "thinking" | null = null;
|
|
73
|
+
|
|
74
|
+
function startBlock(type: "text" | "thinking") {
|
|
75
|
+
contentIndex++;
|
|
76
|
+
currentBlockType = type;
|
|
77
|
+
if (type === "text") {
|
|
78
|
+
output.content.push({ type: "text", text: "" });
|
|
79
|
+
stream.push({ type: "text_start", contentIndex, partial: output });
|
|
80
|
+
} else {
|
|
81
|
+
output.content.push({ type: "thinking", thinking: "" });
|
|
82
|
+
stream.push({ type: "thinking_start", contentIndex, partial: output });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function endBlock() {
|
|
87
|
+
if (currentBlockType === null) return;
|
|
88
|
+
if (currentBlockType === "text") {
|
|
89
|
+
const block = output.content[contentIndex] as TextContent;
|
|
90
|
+
stream.push({ type: "text_end", contentIndex, content: block.text, partial: output });
|
|
91
|
+
} else {
|
|
92
|
+
const block = output.content[contentIndex] as ThinkingContent;
|
|
93
|
+
stream.push({ type: "thinking_end", contentIndex, content: block.thinking, partial: output });
|
|
94
|
+
}
|
|
95
|
+
currentBlockType = null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function emitDelta(type: "text" | "thinking", text: string) {
|
|
99
|
+
if (!text) return;
|
|
100
|
+
if (currentBlockType !== type) {
|
|
101
|
+
endBlock();
|
|
102
|
+
startBlock(type);
|
|
103
|
+
}
|
|
104
|
+
if (type === "text") {
|
|
105
|
+
(output.content[contentIndex] as TextContent).text += text;
|
|
106
|
+
stream.push({ type: "text_delta", contentIndex, delta: text, partial: output });
|
|
107
|
+
} else {
|
|
108
|
+
(output.content[contentIndex] as ThinkingContent).thinking += text;
|
|
109
|
+
stream.push({ type: "thinking_delta", contentIndex, delta: text, partial: output });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function processChunks(chunks: ParsedChunk[]) {
|
|
114
|
+
for (const chunk of chunks) {
|
|
115
|
+
emitDelta(chunk.type, chunk.text);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function processToolCalls(toolCalls: OllamaToolCall[]) {
|
|
120
|
+
endBlock();
|
|
121
|
+
for (const tc of toolCalls) {
|
|
122
|
+
contentIndex++;
|
|
123
|
+
const toolCall: ToolCall = {
|
|
124
|
+
type: "toolCall",
|
|
125
|
+
id: `ollama_tc_${contentIndex}`,
|
|
126
|
+
name: tc.function.name,
|
|
127
|
+
arguments: tc.function.arguments,
|
|
128
|
+
};
|
|
129
|
+
output.content.push(toolCall);
|
|
130
|
+
stream.push({ type: "toolcall_start", contentIndex, partial: output });
|
|
131
|
+
// Emit a delta with the serialized arguments (convention: start/delta/end)
|
|
132
|
+
stream.push({
|
|
133
|
+
type: "toolcall_delta",
|
|
134
|
+
contentIndex,
|
|
135
|
+
delta: JSON.stringify(tc.function.arguments),
|
|
136
|
+
partial: output,
|
|
137
|
+
});
|
|
138
|
+
stream.push({
|
|
139
|
+
type: "toolcall_end",
|
|
140
|
+
contentIndex,
|
|
141
|
+
toolCall,
|
|
142
|
+
partial: output,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
output.stopReason = "toolUse";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for await (const chunk of chat(request, options?.signal)) {
|
|
149
|
+
// Handle text content — process independently of tool_calls
|
|
150
|
+
// (a chunk may contain both content and tool_calls)
|
|
151
|
+
const content = chunk.message?.content ?? "";
|
|
152
|
+
if (content) {
|
|
153
|
+
if (thinkParser) {
|
|
154
|
+
processChunks(thinkParser.push(content));
|
|
155
|
+
} else {
|
|
156
|
+
emitDelta("text", content);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Handle tool calls (Ollama sends them complete, may be on done:true chunk)
|
|
161
|
+
if (chunk.message?.tool_calls?.length) {
|
|
162
|
+
processToolCalls(chunk.message.tool_calls);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (chunk.done) {
|
|
166
|
+
// Final chunk — extract metrics and usage
|
|
167
|
+
if (thinkParser) processChunks(thinkParser.flush());
|
|
168
|
+
endBlock();
|
|
169
|
+
|
|
170
|
+
output.usage = buildUsage(chunk);
|
|
171
|
+
output.inferenceMetrics = extractMetrics(chunk);
|
|
172
|
+
// Preserve "toolUse" if tool calls were processed
|
|
173
|
+
if (output.stopReason !== "toolUse") {
|
|
174
|
+
output.stopReason = mapStopReason(chunk.done_reason);
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
assertStreamSuccess(output, options?.signal);
|
|
181
|
+
finalizeStream(stream, output);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
handleStreamError(stream, output, error, options?.signal);
|
|
184
|
+
}
|
|
185
|
+
})();
|
|
186
|
+
|
|
187
|
+
return stream;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─── Request building ───────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
function buildRequest(
|
|
193
|
+
model: Model<Api>,
|
|
194
|
+
context: Context,
|
|
195
|
+
options?: SimpleStreamOptions,
|
|
196
|
+
): OllamaChatRequest {
|
|
197
|
+
const ollamaOpts = (model.providerOptions ?? {}) as OllamaChatOptions;
|
|
198
|
+
|
|
199
|
+
const request: OllamaChatRequest = {
|
|
200
|
+
model: model.id,
|
|
201
|
+
messages: convertMessages(context),
|
|
202
|
+
stream: true,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Build options block with all Ollama-specific parameters
|
|
206
|
+
const reqOptions: NonNullable<OllamaChatRequest["options"]> = {};
|
|
207
|
+
|
|
208
|
+
// Context window — only sent when explicitly configured via providerOptions.
|
|
209
|
+
// Sending inferred/estimated values risks OOM on constrained hosts.
|
|
210
|
+
// Users can set num_ctx per-model in models.json ollamaOptions or the
|
|
211
|
+
// capability table can provide it for known model families.
|
|
212
|
+
if (ollamaOpts.num_ctx !== undefined && ollamaOpts.num_ctx > 0) {
|
|
213
|
+
reqOptions.num_ctx = ollamaOpts.num_ctx;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Max output tokens
|
|
217
|
+
const maxTokens = options?.maxTokens ?? model.maxTokens;
|
|
218
|
+
if (maxTokens > 0) {
|
|
219
|
+
reqOptions.num_predict = maxTokens;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Temperature
|
|
223
|
+
if (options?.temperature !== undefined) {
|
|
224
|
+
reqOptions.temperature = options.temperature;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Per-model sampling options from providerOptions
|
|
228
|
+
if (ollamaOpts.top_p !== undefined) reqOptions.top_p = ollamaOpts.top_p;
|
|
229
|
+
if (ollamaOpts.top_k !== undefined) reqOptions.top_k = ollamaOpts.top_k;
|
|
230
|
+
if (ollamaOpts.repeat_penalty !== undefined) reqOptions.repeat_penalty = ollamaOpts.repeat_penalty;
|
|
231
|
+
if (ollamaOpts.seed !== undefined) reqOptions.seed = ollamaOpts.seed;
|
|
232
|
+
if (ollamaOpts.num_gpu !== undefined) reqOptions.num_gpu = ollamaOpts.num_gpu;
|
|
233
|
+
|
|
234
|
+
if (Object.keys(reqOptions).length > 0) {
|
|
235
|
+
request.options = reqOptions;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Keep alive
|
|
239
|
+
if (ollamaOpts.keep_alive !== undefined) {
|
|
240
|
+
request.keep_alive = ollamaOpts.keep_alive;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Tools
|
|
244
|
+
if (context.tools?.length) {
|
|
245
|
+
request.tools = convertTools(context.tools);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return request;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ─── Message conversion ─────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
function convertMessages(context: Context): OllamaChatMessage[] {
|
|
254
|
+
const messages: OllamaChatMessage[] = [];
|
|
255
|
+
|
|
256
|
+
// System prompt
|
|
257
|
+
if (context.systemPrompt) {
|
|
258
|
+
messages.push({ role: "system", content: context.systemPrompt });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (const msg of context.messages) {
|
|
262
|
+
switch (msg.role) {
|
|
263
|
+
case "user":
|
|
264
|
+
messages.push(convertUserMessage(msg));
|
|
265
|
+
break;
|
|
266
|
+
case "assistant":
|
|
267
|
+
messages.push(convertAssistantMessage(msg));
|
|
268
|
+
break;
|
|
269
|
+
case "toolResult":
|
|
270
|
+
messages.push({
|
|
271
|
+
role: "tool",
|
|
272
|
+
content: msg.content
|
|
273
|
+
.filter((c): c is TextContent => c.type === "text")
|
|
274
|
+
.map((c) => c.text)
|
|
275
|
+
.join("\n"),
|
|
276
|
+
name: msg.toolName,
|
|
277
|
+
});
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return messages;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function convertUserMessage(msg: Message & { role: "user" }): OllamaChatMessage {
|
|
286
|
+
if (typeof msg.content === "string") {
|
|
287
|
+
return { role: "user", content: msg.content };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const textParts: string[] = [];
|
|
291
|
+
const images: string[] = [];
|
|
292
|
+
|
|
293
|
+
for (const part of msg.content) {
|
|
294
|
+
if (part.type === "text") {
|
|
295
|
+
textParts.push(part.text);
|
|
296
|
+
} else if (part.type === "image") {
|
|
297
|
+
// Strip data URI prefix if present
|
|
298
|
+
let data = (part as ImageContent).data;
|
|
299
|
+
const commaIdx = data.indexOf(",");
|
|
300
|
+
if (commaIdx !== -1 && data.startsWith("data:")) {
|
|
301
|
+
data = data.slice(commaIdx + 1);
|
|
302
|
+
}
|
|
303
|
+
images.push(data);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const result: OllamaChatMessage = {
|
|
308
|
+
role: "user",
|
|
309
|
+
content: textParts.join("\n"),
|
|
310
|
+
};
|
|
311
|
+
if (images.length > 0) {
|
|
312
|
+
result.images = images;
|
|
313
|
+
}
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function convertAssistantMessage(msg: Message & { role: "assistant" }): OllamaChatMessage {
|
|
318
|
+
let content = "";
|
|
319
|
+
const toolCalls: OllamaChatMessage["tool_calls"] = [];
|
|
320
|
+
|
|
321
|
+
for (const block of msg.content) {
|
|
322
|
+
if (block.type === "thinking") {
|
|
323
|
+
// Serialize thinking back inline for round-trip with Ollama
|
|
324
|
+
content += `<think>${(block as ThinkingContent).thinking}</think>`;
|
|
325
|
+
} else if (block.type === "text") {
|
|
326
|
+
content += (block as TextContent).text;
|
|
327
|
+
} else if (block.type === "toolCall") {
|
|
328
|
+
const tc = block as ToolCall;
|
|
329
|
+
toolCalls.push({
|
|
330
|
+
function: {
|
|
331
|
+
name: tc.name,
|
|
332
|
+
arguments: tc.arguments,
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const result: OllamaChatMessage = { role: "assistant", content };
|
|
339
|
+
if (toolCalls.length > 0) {
|
|
340
|
+
result.tool_calls = toolCalls;
|
|
341
|
+
}
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ─── Tool conversion ────────────────────────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
function convertTools(tools: Tool[]): OllamaTool[] {
|
|
348
|
+
return tools.map((tool) => {
|
|
349
|
+
const params = tool.parameters as Record<string, unknown>;
|
|
350
|
+
return {
|
|
351
|
+
type: "function" as const,
|
|
352
|
+
function: {
|
|
353
|
+
name: tool.name,
|
|
354
|
+
description: tool.description,
|
|
355
|
+
parameters: {
|
|
356
|
+
type: "object" as const,
|
|
357
|
+
required: params.required as string[] | undefined,
|
|
358
|
+
properties: (params.properties as Record<string, unknown>) ?? {},
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ─── Response mapping ───────────────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
function mapStopReason(doneReason?: string): StopReason {
|
|
368
|
+
switch (doneReason) {
|
|
369
|
+
case "stop":
|
|
370
|
+
return "stop";
|
|
371
|
+
case "length":
|
|
372
|
+
return "length";
|
|
373
|
+
default:
|
|
374
|
+
return "stop";
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function buildUsage(chunk: OllamaChatResponse): Usage {
|
|
379
|
+
const input = chunk.prompt_eval_count ?? 0;
|
|
380
|
+
const outputTokens = chunk.eval_count ?? 0;
|
|
381
|
+
return {
|
|
382
|
+
input,
|
|
383
|
+
output: outputTokens,
|
|
384
|
+
cacheRead: 0,
|
|
385
|
+
cacheWrite: 0,
|
|
386
|
+
totalTokens: input + outputTokens,
|
|
387
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function extractMetrics(chunk: OllamaChatResponse): InferenceMetrics | undefined {
|
|
392
|
+
if (!chunk.eval_duration && !chunk.total_duration) return undefined;
|
|
393
|
+
|
|
394
|
+
const evalCount = chunk.eval_count ?? 0;
|
|
395
|
+
const evalDurationNs = chunk.eval_duration ?? 0;
|
|
396
|
+
const evalDurationMs = evalDurationNs / 1e6;
|
|
397
|
+
const tokensPerSecond = evalDurationNs > 0 ? evalCount / (evalDurationNs / 1e9) : 0;
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
tokensPerSecond,
|
|
401
|
+
totalDurationMs: (chunk.total_duration ?? 0) / 1e6,
|
|
402
|
+
evalDurationMs,
|
|
403
|
+
promptEvalDurationMs: (chunk.prompt_eval_duration ?? 0) / 1e6,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ─── Stream lifecycle helpers ───────────────────────────────────────────────
|
|
408
|
+
// Replicated from openai-shared.ts (not exported from @gsd/pi-ai)
|
|
409
|
+
|
|
410
|
+
function buildInitialOutput(model: Model<Api>): AssistantMessage {
|
|
411
|
+
return {
|
|
412
|
+
role: "assistant",
|
|
413
|
+
content: [],
|
|
414
|
+
api: model.api as Api,
|
|
415
|
+
provider: model.provider,
|
|
416
|
+
model: model.id,
|
|
417
|
+
usage: {
|
|
418
|
+
input: 0,
|
|
419
|
+
output: 0,
|
|
420
|
+
cacheRead: 0,
|
|
421
|
+
cacheWrite: 0,
|
|
422
|
+
totalTokens: 0,
|
|
423
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
424
|
+
},
|
|
425
|
+
stopReason: "stop",
|
|
426
|
+
timestamp: Date.now(),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function assertStreamSuccess(output: AssistantMessage, signal?: AbortSignal): void {
|
|
431
|
+
if (signal?.aborted) {
|
|
432
|
+
throw new Error("Request was aborted");
|
|
433
|
+
}
|
|
434
|
+
if (output.stopReason === "aborted" || output.stopReason === "error") {
|
|
435
|
+
throw new Error("An unknown error occurred");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function finalizeStream(stream: AssistantMessageEventStream, output: AssistantMessage): void {
|
|
440
|
+
stream.push({
|
|
441
|
+
type: "done",
|
|
442
|
+
reason: output.stopReason as Extract<StopReason, "stop" | "length" | "toolUse" | "pauseTurn">,
|
|
443
|
+
message: output,
|
|
444
|
+
});
|
|
445
|
+
stream.end();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function handleStreamError(
|
|
449
|
+
stream: AssistantMessageEventStream,
|
|
450
|
+
output: AssistantMessage,
|
|
451
|
+
error: unknown,
|
|
452
|
+
signal?: AbortSignal,
|
|
453
|
+
): void {
|
|
454
|
+
for (const block of output.content) delete (block as { index?: number }).index;
|
|
455
|
+
output.stopReason = signal?.aborted ? "aborted" : "error";
|
|
456
|
+
output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
457
|
+
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
458
|
+
stream.end();
|
|
459
|
+
}
|
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type {
|
|
11
|
+
OllamaChatRequest,
|
|
12
|
+
OllamaChatResponse,
|
|
11
13
|
OllamaPsResponse,
|
|
12
14
|
OllamaPullProgress,
|
|
13
15
|
OllamaShowResponse,
|
|
14
16
|
OllamaTagsResponse,
|
|
15
17
|
OllamaVersionResponse,
|
|
16
18
|
} from "./types.js";
|
|
19
|
+
import { parseNDJsonStream } from "./ndjson-stream.js";
|
|
17
20
|
|
|
18
21
|
const DEFAULT_HOST = "http://localhost:11434";
|
|
19
22
|
const PROBE_TIMEOUT_MS = 1500;
|
|
@@ -130,39 +133,36 @@ export async function pullModel(
|
|
|
130
133
|
throw new Error("Ollama /api/pull returned no body");
|
|
131
134
|
}
|
|
132
135
|
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
let buffer = "";
|
|
136
|
-
|
|
137
|
-
while (true) {
|
|
138
|
-
const { done, value } = await reader.read();
|
|
139
|
-
if (done) break;
|
|
140
|
-
|
|
141
|
-
buffer += decoder.decode(value, { stream: true });
|
|
142
|
-
const lines = buffer.split("\n");
|
|
143
|
-
buffer = lines.pop() ?? "";
|
|
144
|
-
|
|
145
|
-
for (const line of lines) {
|
|
146
|
-
const trimmed = line.trim();
|
|
147
|
-
if (!trimmed) continue;
|
|
148
|
-
try {
|
|
149
|
-
const progress = JSON.parse(trimmed) as OllamaPullProgress;
|
|
150
|
-
onProgress?.(progress);
|
|
151
|
-
} catch {
|
|
152
|
-
// Skip malformed lines
|
|
153
|
-
}
|
|
154
|
-
}
|
|
136
|
+
for await (const progress of parseNDJsonStream<OllamaPullProgress>(response.body, signal)) {
|
|
137
|
+
onProgress?.(progress);
|
|
155
138
|
}
|
|
139
|
+
}
|
|
156
140
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Stream a chat completion via /api/chat.
|
|
143
|
+
* Returns an async generator yielding each NDJSON response chunk.
|
|
144
|
+
*/
|
|
145
|
+
export async function* chat(
|
|
146
|
+
request: OllamaChatRequest,
|
|
147
|
+
signal?: AbortSignal,
|
|
148
|
+
): AsyncGenerator<OllamaChatResponse> {
|
|
149
|
+
const response = await fetch(`${getOllamaHost()}/api/chat`, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: { "Content-Type": "application/json" },
|
|
152
|
+
body: JSON.stringify(request),
|
|
153
|
+
signal,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
const text = await response.text();
|
|
158
|
+
throw new Error(`Ollama /api/chat returned ${response.status}: ${text}`);
|
|
165
159
|
}
|
|
160
|
+
|
|
161
|
+
if (!response.body) {
|
|
162
|
+
throw new Error("Ollama /api/chat returned no body");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
yield* parseNDJsonStream<OllamaChatResponse>(response.body, signal, true);
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/**
|
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
* Returns models in the format expected by pi.registerProvider().
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { listModels
|
|
11
|
+
import { listModels } from "./ollama-client.js";
|
|
12
12
|
import {
|
|
13
13
|
estimateContextFromParams,
|
|
14
14
|
formatModelSize,
|
|
15
15
|
getModelCapabilities,
|
|
16
16
|
humanizeModelName,
|
|
17
17
|
} from "./model-capabilities.js";
|
|
18
|
-
import type { OllamaModelInfo } from "./types.js";
|
|
18
|
+
import type { OllamaChatOptions, OllamaModelInfo } from "./types.js";
|
|
19
19
|
|
|
20
20
|
export interface DiscoveredOllamaModel {
|
|
21
21
|
id: string;
|
|
@@ -29,6 +29,8 @@ export interface DiscoveredOllamaModel {
|
|
|
29
29
|
sizeBytes: number;
|
|
30
30
|
/** Parameter size string from Ollama (e.g. "7B") */
|
|
31
31
|
parameterSize: string;
|
|
32
|
+
/** Ollama-specific inference options for this model */
|
|
33
|
+
ollamaOptions?: OllamaChatOptions;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
const ZERO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
@@ -64,6 +66,7 @@ function enrichModel(info: OllamaModelInfo): DiscoveredOllamaModel {
|
|
|
64
66
|
maxTokens,
|
|
65
67
|
sizeBytes: info.size,
|
|
66
68
|
parameterSize,
|
|
69
|
+
ollamaOptions: caps.ollamaOptions,
|
|
67
70
|
};
|
|
68
71
|
}
|
|
69
72
|
|
|
@@ -98,9 +101,3 @@ export function formatModelForDisplay(model: DiscoveredOllamaModel): string {
|
|
|
98
101
|
return parts.join(" ");
|
|
99
102
|
}
|
|
100
103
|
|
|
101
|
-
/**
|
|
102
|
-
* Build the OpenAI-compat base URL for Ollama.
|
|
103
|
-
*/
|
|
104
|
-
export function getOllamaOpenAIBaseUrl(): string {
|
|
105
|
-
return `${getOllamaHost()}/v1`;
|
|
106
|
-
}
|
|
@@ -31,6 +31,8 @@ export function registerOllamaTool(pi: ExtensionAPI): void {
|
|
|
31
31
|
promptGuidelines: [
|
|
32
32
|
"Use 'list' to see what models are available locally before trying to use one.",
|
|
33
33
|
"Use 'pull' to download a model that isn't available yet.",
|
|
34
|
+
"Use 'remove' to delete a local model that is no longer needed.",
|
|
35
|
+
"Use 'show' to get detailed info about a model (parameters, quantization, families).",
|
|
34
36
|
"Use 'status' to check if Ollama is running.",
|
|
35
37
|
"Use 'ps' to see which models are loaded in memory and VRAM usage.",
|
|
36
38
|
"Common models: llama3.1:8b, qwen2.5-coder:7b, deepseek-r1:8b, codestral:22b",
|
|
@@ -40,6 +42,8 @@ export function registerOllamaTool(pi: ExtensionAPI): void {
|
|
|
40
42
|
[
|
|
41
43
|
Type.Literal("list"),
|
|
42
44
|
Type.Literal("pull"),
|
|
45
|
+
Type.Literal("remove"),
|
|
46
|
+
Type.Literal("show"),
|
|
43
47
|
Type.Literal("status"),
|
|
44
48
|
Type.Literal("ps"),
|
|
45
49
|
],
|
|
@@ -164,6 +168,71 @@ export function registerOllamaTool(pi: ExtensionAPI): void {
|
|
|
164
168
|
};
|
|
165
169
|
}
|
|
166
170
|
|
|
171
|
+
case "remove": {
|
|
172
|
+
if (!model) {
|
|
173
|
+
return {
|
|
174
|
+
content: [{ type: "text", text: "Error: 'model' parameter is required for remove action." }],
|
|
175
|
+
isError: true,
|
|
176
|
+
details: { action, durationMs: Date.now() - startTime, error: "missing_model" } as OllamaToolDetails,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const running = await client.isRunning();
|
|
181
|
+
if (!running) {
|
|
182
|
+
return {
|
|
183
|
+
content: [{ type: "text", text: "Ollama is not running." }],
|
|
184
|
+
isError: true,
|
|
185
|
+
details: { action, model, durationMs: Date.now() - startTime, error: "not_running" } as OllamaToolDetails,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await client.deleteModel(model);
|
|
190
|
+
return {
|
|
191
|
+
content: [{ type: "text", text: `Successfully removed ${model}` }],
|
|
192
|
+
details: { action, model, durationMs: Date.now() - startTime } as OllamaToolDetails,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
case "show": {
|
|
197
|
+
if (!model) {
|
|
198
|
+
return {
|
|
199
|
+
content: [{ type: "text", text: "Error: 'model' parameter is required for show action." }],
|
|
200
|
+
isError: true,
|
|
201
|
+
details: { action, durationMs: Date.now() - startTime, error: "missing_model" } as OllamaToolDetails,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const running = await client.isRunning();
|
|
206
|
+
if (!running) {
|
|
207
|
+
return {
|
|
208
|
+
content: [{ type: "text", text: "Ollama is not running." }],
|
|
209
|
+
isError: true,
|
|
210
|
+
details: { action, model, durationMs: Date.now() - startTime, error: "not_running" } as OllamaToolDetails,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const info = await client.showModel(model);
|
|
215
|
+
const details = info.details;
|
|
216
|
+
const infoLines = [
|
|
217
|
+
`Model: ${model}`,
|
|
218
|
+
`Family: ${details.family}`,
|
|
219
|
+
`Parameters: ${details.parameter_size}`,
|
|
220
|
+
`Quantization: ${details.quantization_level}`,
|
|
221
|
+
`Format: ${details.format}`,
|
|
222
|
+
];
|
|
223
|
+
if (details.families?.length) {
|
|
224
|
+
infoLines.push(`Families: ${details.families.join(", ")}`);
|
|
225
|
+
}
|
|
226
|
+
if (info.parameters) {
|
|
227
|
+
infoLines.push(`\nModelfile parameters:\n${info.parameters}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
content: [{ type: "text", text: infoLines.join("\n") }],
|
|
232
|
+
details: { action, model, durationMs: Date.now() - startTime } as OllamaToolDetails,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
167
236
|
default:
|
|
168
237
|
return {
|
|
169
238
|
content: [{ type: "text", text: `Unknown action: ${action}` }],
|