Add reasoning effort selection and streaming responses - #21
Merged
Conversation
- Upgrade `@anthropic-ai/sdk` from ^0.87.0 to 0.90.0. - Replace `@google/generative-ai` with `@google/genai` 1.50.1. - Upgrade `openai` from ^6.25.0 to 6.34.0. - Pin all three AI SDK versions instead of using caret ranges. - Add transitive dependencies including `protobufjs`, `p-retry`, and `google-auth-library`.
- Introduce per-provider `effort` field on `ProviderConfig` for OpenAI, Anthropic, and Gemini, with SDK-backed value lists and compile-time exhaustiveness checks in `domain/config/config`. - Add `domain/llm/effort` module with param builders (`openaiReasoningParam`, `anthropicAdaptiveParam`, `anthropicEnabledParam`, `geminiLevelConfig`, `geminiBudgetConfig`) and helpers to seed and update provider config. - Add `infra/llm/effort-fallback` with `tryWithEffort` to retry API calls when the SDK rejects effort parameters. - Wire effort into `infra/llm/openai`, `infra/llm/anthropic`, and `infra/llm/gemini` call paths, including adaptive/enabled thinking stages for Anthropic and level/budget stages for Gemini. - Migrate Gemini integration from `@google/generative-ai` to `@google/genai` and adjust `response-parser` to the new SDK shape. - Add `infra/ui/effort-picker` and `infra/ui/effort-slider` Ink component, and prompt for effort during `setup` and `model` commands. - Preserve `effort` when refreshing OAuth tokens in `auth-resolver` and `infra/storage/config`. - Add `repo.getBaseBranch` using reflog parsing with fallback to `origin/HEAD`, and a `not-found` variant to `pr.PrLookup`. - Extend `doctor` with a git context section showing branch, base, and open PR rows. - Make push metadata fields optional in `cli/commit` and `infra/ui/push-note`, warning per field and skipping the note when nothing is available. - Surface the selected effort next to the model in `doctor` output via `renderModelInfo`.
- Add `findCurrentBranch`, `findBaseBranch`, `findCommitMetadata`, and `findTrackingRemoteUrl` to return `Nothing` instead of failing on missing git metadata. - Update commit and doctor flows to use the new lookup helpers and remove duplicated local error recovery logic. - Let push note rendering consume `Maybe` values directly with `maybe(...)` instead of checking `Just` instances manually. - Remove fallback warning handling around push note metadata collection now that lookups recover gracefully.
- Change `ExecResult` to `Result<CommandFailure, CommandOutput>` with explicit success and failure branches. - Introduce `CommandOutput` and `CommandFailure` types carrying stdout, stderr, and an `Error` with exit-code context. - Update all `execBin` callers in `repo.ts` and `pr.ts` to consume results via `result.either(...)` instead of inspecting `exitCode`. - Add `commandFailureMessage` helper to build error messages from stderr, stdout, or the underlying error. - Extract `formatCommitOutput` helper for post-commit stdout formatting. - Rename `reflog-cmd-failed.stderr` to `message` and use `commandFailureMessage` when constructing it. - Update `classifyFailure` in `pr.ts` to accept a `CommandFailure` and match patterns against combined failure text. - Wrap spawn `error` events with a clearer "Failed to start process" message.
- Bump `printWidth` from 120 to 154 in `.prettierrc` and update the `CONVENTIONS.md` example accordingly. - Reflow `pnpm-lock.yaml` `resolution` entries onto single lines under the new width. - Collapse multi-line function signatures, conditionals, and expressions across `src/` to fit the wider line limit.
- Replace provider-tagged `RawResponse` union with a single `{ text: Maybe<string> }` type in `domain/llm/response-parser`.
- Move Anthropic content extraction (`extractAnthropicText`) into `infra/llm/anthropic.ts`.
- Move OpenAI stream text extraction (`extractStreamText`) into `infra/llm/openai.ts`.
- Wrap Gemini SDK and REST text into `Maybe<string>` via `fromOptional` at the call sites.
- Drop `finalizeText`, provider-specific types, and the switch-based dispatch from the domain layer.
- Remove `anthropicAdaptiveParam`, `anthropicEnabledParam`, and the `BUDGET_BY_EFFORT` map from `src/domain/llm/effort.ts`. - Drop the multi-stage effort fallback (`adaptive`/`enabled`/`off`) and `tryWithEffort` usage in the Anthropic client. - Consolidate API key and setup token request building into a single `buildParams` helper that always enables adaptive thinking and defaults effort to `medium`. - Extract `buildSetupTokenSystem` to compose the Claude Code system prompt with the optional user instruction via `Maybe`. - Replace the generic `toError` with inline error wrapping that surfaces a clearer `Failed to create Anthropic message` message.
- Replace custom `TextBlock` type with `Anthropic.ContentBlock` and `Anthropic.TextBlock` in `extractAnthropicText`. - Switch `buildSetupTokenSystem` to return `Anthropic.TextBlockParam[]` and inline the Claude Code system block. - Drop the standalone `claudeCodeBlock` constant. - Increase `BASE_MAX_TOKENS` from 4096 to 16384.
- Remove `effort-fallback` module and its `tryWithEffort` retry mechanism. - Add shared `unsupportedAuth` helper in `domain/llm/auth-error` for consistent rejection of unsupported auth methods. - Simplify Gemini integration to use a single `thinkingLevel` config with a `MEDIUM` default, dropping multi-stage attempts and budget-based fallbacks. - Simplify OpenAI integration to build request params directly via SDK types, replacing reasoning fallback attempts with a single call. - Remove now-unused `openaiReasoningParam`, `geminiLevelConfig`, and `geminiBudgetConfig` from `domain/llm/effort`. - Use `absurd` for exhaustive auth method handling in Anthropic, Gemini, and OpenAI providers. - Standardize error messages for failed Gemini and OpenAI requests and introduce a shared `BASE_MAX_TOKENS` cap.
- Measure and display elapsed time in `doctor` run output. - Pass `elapsedMs` through `renderTable` to report duration on success. - Strip `refs/remotes/<remote>/` prefix in `normalizeBranchRef`. - Fall back to default remote branch in `getBaseBranch` when the resolved base matches the current branch.
- Stop sending `maxOutputTokens` for Gemini SDK and OAuth requests. - Stop sending `max_output_tokens` for OpenAI API key and OAuth requests. - Guard OpenAI stream text extraction against non-string candidates before trimming.
- Add immutable `Queue` with amortised O(1) `enqueue` and `dequeue` using front/back list pair. - Add mutable `MQueue` wrapper around the immutable `Queue`. - Add `MVar` for state-based coordination of concurrent operations, blocking on full or empty state. - Support blocking `put`, `take`, `modify`, `modify_`, and `read` operations with FIFO fairness. - Provide non-blocking `tryPut`, `tryTake`, and `tryRead` variants returning success or `Maybe` results.
- Update `generateContentWithApiKey` to use the SDK's `generateContentStream` method. - Update `generateContentWithOAuth` to call the `streamGenerateContent` endpoint and parse SSE responses. - Add `accumulateSSEText` and `extractSSEEventText` helpers to handle stream data processing. - Remove redundant provider check in `getAuthCredentials`.
- Wrap single-parameter arrow function params in parentheses across `mvar.ts` and `queue.ts`. - Remove trailing commas from function call argument lists. - Collapse some multi-line expressions onto a single line.
- Replace terse inline comments with JSDoc on `MVar`, `Queue`, and `MQueue` methods, covering blocking semantics, return types, and rollback behaviour of `modify`. - Rewrite the `MVar` section of `CONVENTIONS.md` with concrete usage patterns: completion/error latch, end-of-stream latch via `AsyncIterable`, and atomic state mutation through `modify`. - Add a new `Queue` & `MQueue` conventions section explaining the persistent vs. transient distinction and the amortised O(1) two-list representation. - Drop `BoundedBuffer` guidance from the conventions in favour of the updated `MVar` patterns.
- Replace newline-based git log format with `%x00`-separated fields to avoid ambiguity from multi-line subjects. - Introduce `commitMetadataDecoder` along with `nonEmptyString` and `isoDate` decoders for stricter field validation. - Add `splitCommitFields` helper to map raw git output to a structured record before decoding. - Surface decoder error messages in the rejected `Error` for better diagnostics.
- Replace `new Promise` inside `Future.attemptP` with `Future.create` chained after async imports in both `effort-picker` and `model-picker`. - Remove error-based cancellation workaround in `effort-picker`; resolve with `Nothing` directly on cancel instead of rejecting and catching in `chainRej`. - Return `unmount` as the teardown function from `Future.create` for proper cleanup.
- Relocate `resolveAuthMethod` from `auth-resolver.ts` to `config.ts` so it can be reused across modules. - Refactor `updateGoogleTokens` and `updateOpenAITokens` to use exhaustive `switch` statements with `absurd` for type-safe handling of all auth methods. - Replace ad-hoc object spreads with `resolveAuthMethod` when persisting refreshed tokens.
- Document why `selectEffort` uses dynamic `import()` to avoid Ink/React startup cost on non-interactive CLI paths. - Add the same rationale to `selectModelInteractively`, calling out scripted runs, `--yes` flows, piped stdin, and git hook integrations.
- Add an optional notice area to the success auth template. - Add a Google OAuth notice explaining that terminal completion can take 1-2 minutes. - Use `GOOGLE_SUCCESS_HTML` for the Google OAuth callback response.
- Add reusable `match` and `search` helpers for scored fuzzy matching. - Update the model selector to rank results across model IDs and descriptions.
- Register the `effort` CLI command and route it to a new interactive effort update flow. - Persist the selected reasoning effort for the current provider in the stored AI config. - Update help output, version reporting, and README docs for `commit effort`.
- Read `package.json` version in `showVersion` instead of hardcoding the CLI version. - Extend the `@/*` path alias to resolve root-level imports such as `package.json`.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bd4514cd8b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
This PR introduces features and code-quality improvements. The most significant changes are the effort selector, new metrics added to the
doctorcommand, and refactors in model search. Enjoy!What's New
Reasoning Effort Selection
src/domain/llm/effort.tswith provider-agnosticseedProviderConfig,withModel, andselectEffortForProviderEffortSliderInk component (src/infra/ui/effort-slider.tsx) with arrow-key navigation and a chalk palette per option countOpenAIEffort,AnthropicEffort,GeminiEffortenums typed against SDK reasoning unions, defaulting tomediumsetupandmodelcommands now run the picker after model selection, persisting effort per-provider inConfigcommit effortcommand (src/cli/effort.ts) updates effort for the current provider without re-selecting a modelStreaming LLM Responses
messages.stream(...).finalMessage()withthinking: { type: "adaptive" }andoutput_config.effortapi_key,openai_oauth, and ChatGPT subscription auth paths withreasoning.effort@google/generative-aito@google/genai, accumulating SSE events fromstreamGenerateContent?alt=ssewiththinkingConfig.thinkingLevelextractResponsecollapsed to a provider-agnostic{ text: Maybe<string> }shape; provider-specific extraction now lives next to each clientmax_tokensraised to16384Doctor Command Diagnostics
checkGitContextstep addsBranch,Base, andPull Requestrows alongside runtime/platform/auth checksperformance.now()timing renders asDone in X.XXsafter the tableProviderrow showsmodel (effort)when effort is configuredModel Search & Picker
src/libs/fuzzy.tsexposesmatchandsearchwith token-based scoring (boundary, consecutive, gap penalties)ModelSelectoradopts the shared fuzzy module, replacing its inline matchermodel-pickerrefactored fromnew Promise(...)wrapper toFuture.create, with lazy Ink/React imports documented inlineConcurrency Primitives
MVar<T>insrc/libs/mvar.ts— FIFO async coordination with blockingput/take/read/modifyand non-blockingtryPut/tryTake/tryRead;modifyrolls back on rejectionQueue<T>(immutable, persistent) andMQueue<T>(mutable, transient) insrc/libs/queue.ts— two-list representation, amortised O(1) enqueue/dequeueCONVENTIONS.mddocuments completion latch (MVar<Maybe<Error>>), end-of-stream latch (MVar<null>), and atomic-state (modify) patternsAuth & OAuth
resolveAuthMethodlifted intosrc/domain/config/config.tsso token-refresh paths preserveeffortunsupportedAuthhelper insrc/domain/llm/auth-error.tsreplaces inlineFuture.rejectfor unsupported provider/auth combinationsGOOGLE_SUCCESS_HTML) instead of the generic templategh pr viewlookup distinguishes a newnot-foundstate fromunavailable/unauthenticatedShell & Git Refactors
execBinreturnsResult<CommandFailure, CommandOutput>; callers pattern-match viaresult.either(...)instead of inspectingexitCodefindCurrentBranch,findBaseBranch,findCommitMetadata,findTrackingRemoteUrlreturnFuture<Error, Maybe<T>>so push notes degrade gracefully when context is missingbranch: Created from <ref>parsing with named regex helpersDecoderrather than ad-hoc string slicingghfailure classification usesisGhUnauthenticatedandisGhPrNotFoundnamed predicatesConventions & Tooling
CONVENTIONS.mddocumentsMVar/Queue/MQueue; immutable-snapshot guidance removedprintWidthraised from 120 to 154; codebase reformatted accordingly@anthropic-ai/sdk@0.90.0,@google/genai@1.50.1,openai@6.34.0pinnedpackage.jsonversion bumped to0.2.5; CLI--versionnow reads frompackage.jsonvia the@/*alias (extended intsconfig.jsonto resolve root)commit effortcommand and bumped version badgePLAN.mdandwithAuthMethodTODO removedReasoning Effort Flow
Changed Files
.prettierrcprintWidthfrom 120 to 154CONVENTIONS.mdMVar/Queue/MQueue; remove immutable-snapshot guidanceREADME.md0.2.5; documentcommit effortcommandindex.tsEffortCommandinto the CLI dispatcherpackage.json0.2.5; swap@google/generative-ai→@google/genai; pin AI SDKspnpm-lock.yamltsconfig.json@/*paths to resolve root-level imports (e.g.package.json)src/cli/commit.tsMaybe-returning git lookups; allow partial push-note metadatasrc/cli/doctor.tssrc/cli/effort.tscommit effortcommand updating reasoning effort for current providersrc/cli/model.tsselectEffortForProviderafter model selectionsrc/cli/parser.tseffortcommand, read CLI version frompackage.jsonsrc/cli/setup.tsProviderConfigand prompt for effort during setupsrc/domain/commit/models.tssrc/domain/commit/prompts.tssrc/domain/config/config.tsOpenAIEffort/AnthropicEffort/GeminiEffort, schemas, andresolveAuthMethodsrc/domain/llm/auth-error.tsunsupportedAuthhelper for provider/auth-method mismatchessrc/domain/llm/auth-resolver.tsresolveAuthMethodto preserveeffortacross token refreshsrc/domain/llm/effort.tsseedProviderConfig,withModel,selectEffortForProvidersrc/domain/llm/response-parser.ts{ text: Maybe<string> }; remove provider branchessrc/domain/llm/router.tsProviderConfigthrough; clients own effort wiringsrc/infra/auth/google.tsGOOGLE_SUCCESS_HTML; reflow OAuth bracketsrc/infra/auth/openai.tsResult-basedexecBin; reflow templatessrc/infra/auth/templates.tssrc/infra/git/repo.tsfind*Maybelookups, reflog-basedgetBaseBranch, NUL-separated metadata,ResultexecBinmigrationsrc/infra/github/pr.tsnot-foundPrLookupvariant; named regex helpers; consumeCommandFailuresrc/infra/llm/anthropic.tsmessages.stream(...).finalMessage()with adaptive thinking andoutput_config.effortsrc/infra/llm/gemini.ts@google/genai; SSE streaming withthinkingLevelsrc/infra/llm/openai.tsreasoning.effortsrc/infra/shell.tsexecBinreturnsResult<CommandFailure, CommandOutput>src/infra/storage/config.tsresolveAuthMethodsrc/infra/ui/effort-picker.tssrc/infra/ui/effort-slider.tsxEffortSliderInk component (arrow-key nav, chalk palette)src/infra/ui/model-picker.tsFuture.create; lazy Ink/React import comments expandedsrc/infra/ui/model-selector.tsxlibs/fuzzyfor filteringsrc/infra/ui/push-note.tsMaybemetadata; addbaseline; handlenot-foundPR casesrc/libs/future.tssrc/libs/fuzzy.tssrc/libs/helpers/object.tssrc/libs/json/decoder.tssrc/libs/json/encoder.tssrc/libs/json/schema.tssrc/libs/maybe.tssrc/libs/mvar.tsMVar<T>async coordination primitive withmodifyrollbacksrc/libs/queue.tsQueue<T>immutable persistent queue andMQueue<T>companionsrc/libs/remote-data.tssrc/libs/router.tssrc/libs/time.tssrc/libs/trampoline.tssrc/libs/types.tsTesting & Feedback
commit-tools setup,commit-tools model, andcommit-tools effortand confirm the slider appears in each flow and the chosen value persists in the config file.commit-tools --versionand confirm it reflects the version frompackage.json(currently0.2.5).commit-tools doctorinside and outside a git repository — verify the newBranch/Base/Pull Requestrows and theDone in X.XXsfooter.anthropic,openai,gemini) with bothapi_keyand OAuth auth paths to validate streaming and the new effort flag.Maybe-typed metadata and the newnot-foundclassification.commit-toolsstartup time is unaffected on non-interactive entrypoints (lazy Ink/React import path).If you find any bugs or have recommendations for improvements, please open an issue and assign it to me.