@polderlabs/bizar-plugin 0.6.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.ts +90 -40
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -125,7 +125,7 @@ import { SettingsStore } from "./src/settings.js";
125
125
  import { parseSlashCommand } from "./src/commands.js";
126
126
  import { createPlanActionTool } from "./src/tools/plan-action.js";
127
127
  import { createWaitForFeedbackTool } from "./src/tools/wait-for-feedback.js";
128
- import { wrapFetchForReasoningCleanup } from "./src/reasoning-clean.js";
128
+ import { stripInlineThinkBlocks } from "./src/reasoning-clean.js";
129
129
 
130
130
  // v0.5.0 — visual plan wiring: side-effect executor + plan-fs
131
131
  import { executeSideEffect, type ExecuteOptions } from "./src/commands-impl.js";
@@ -700,6 +700,47 @@ async function listPlanSlugs(worktree: string, logger: Logger): Promise<string[]
700
700
  * delegates to the runtime context and the supporting modules.
701
701
  */
702
702
  function buildHooks(ctx: RuntimeContext, bg: BgDeps): Hooks {
703
+ // ────────────────────────────────────────────────────────────────────
704
+ // v0.6.2 — Reasoning directive
705
+ // ────────────────────────────────────────────────────────────────────
706
+ // Some reasoning models (notably MiniMax M3 via OpenRouter) emit
707
+ // their chain-of-thought in BOTH the structured `reasoning` /
708
+ // `reasoning_details` field AND inline as `` blocks inside
709
+ // `message.content`. opencode's openrouter SDK extracts the structured
710
+ // reasoning correctly and renders it as a separate "Thought" panel,
711
+ // but it does NOT strip the inline blocks from `content`, so the user
712
+ // sees the same thinking text twice — once in the proper panel and
713
+ // again as visible message text below it.
714
+ //
715
+ // The opencode plugin API in this version does NOT trigger a
716
+ // `config` hook (the `wrap-fetch` workaround from v0.6.1 is dead
717
+ // code in current builds), so we cannot post-process the response
718
+ // stream. The only working hooks that can help are:
719
+ //
720
+ // 1. `experimental.chat.system.transform` — runs every turn; we
721
+ // push a directive telling the model to put thinking in the
722
+ // structured field only.
723
+ // 2. `experimental.chat.messages.transform` — runs before each
724
+ // request; we strip `` blocks from previous assistant
725
+ // messages so the model sees clean history and is less likely
726
+ // to keep emitting inline ``.
727
+ //
728
+ // Neither fixes the CURRENT response (the model has already
729
+ // returned), but together they strongly reduce — and in many cases
730
+ // eliminate — the duplication on subsequent turns.
731
+ const REASONING_DIRECTIVE_MARKER = "BIZAR_REASONING_DIRECTIVE_v0.6.2";
732
+ const REASONING_DIRECTIVE = [
733
+ REASONING_DIRECTIVE_MARKER,
734
+ "",
735
+ "When reasoning is enabled for this conversation, output your thinking",
736
+ "ONLY in the model's structured reasoning field. Do NOT emit `` blocks",
737
+ "inline inside your message content — the opencode host extracts the",
738
+ "reasoning field and renders it as a separate, collapsable \"Thought\"",
739
+ "panel. If you also emit the same text inline, the user will see your",
740
+ "thinking twice (once in the panel and once as visible message body).",
741
+ "Keep the actual response text in the normal content stream.",
742
+ ].join(" ");
743
+
703
744
  // Build the 7 tools. We always register them; if the serve child is
704
745
  // not available, the background tools return a clear error. The
705
746
  // bizar_get_plan_comments, bizar_plan_action, and
@@ -707,7 +748,7 @@ function buildHooks(ctx: RuntimeContext, bg: BgDeps): Hooks {
707
748
  // work regardless of the serve child's state.
708
749
  //
709
750
  // v0.4.0 — added `bizar_plan_action` (CRUD on the v2 canvas) and
710
- // `bizar_wait_for_feedback` (poll until feedback). Both are pure
751
+ // bizar_wait_for_feedback (poll until feedback). Both are pure
711
752
  // file I/O — no serve child required.
712
753
  //
713
754
  // v0.5.0 — renamed `bizarre_*` → `bizar_*` (single `r`) to match
@@ -756,36 +797,57 @@ function buildHooks(ctx: RuntimeContext, bg: BgDeps): Hooks {
756
797
  };
757
798
 
758
799
  return {
759
- // §3.1 config: wrap provider fetches to strip duplicated inline
760
- // think blocks from responses of reasoning models that emit BOTH a
761
- // structured reasoning field (rendered as a thought) AND an inline
762
- // `` block (which would otherwise leak into the visible message).
763
- // See plugins/bizar/src/reasoning-clean.ts for the full rationale.
764
- config: async (cfg) => {
800
+ // Push a persistent system-prompt directive that tells reasoning
801
+ // models to put their thinking in the structured reasoning field
802
+ // (rendered as a separate "Thought" panel by opencode) rather than
803
+ // also emitting it inline as `` blocks in the content. The openrouter
804
+ // SDK does not strip the inline `` blocks, so without this
805
+ // directive the user sees the reasoning twice — once in the proper
806
+ // panel and once as visible message text.
807
+ //
808
+ // We push the directive only when the marker is absent so we don't
809
+ // append it on every turn.
810
+ "experimental.chat.system.transform": async (input, output) => {
811
+ const sessionID = input.sessionID;
812
+ if (!sessionID) return;
813
+ if (!output.system.some((s) => s.includes(REASONING_DIRECTIVE_MARKER))) {
814
+ output.system.push(REASONING_DIRECTIVE);
815
+ }
816
+ // §3.1, §5.4 — handoff injection point. We push a single string
817
+ // onto `output.system` if a pending injection is queued for this
818
+ // session.
819
+ const pending = ctx.pendingInjections.get(sessionID);
820
+ if (pending) {
821
+ output.system.push(pending);
822
+ ctx.pendingInjections.delete(sessionID);
823
+ }
824
+ },
825
+
826
+ // Before the model is called, strip `` blocks from
827
+ // any text content in previous assistant messages. This keeps the
828
+ // model's view of its own history clean of the duplicated thinking
829
+ // it emitted earlier, reducing the chance it will keep emitting
830
+ // inline `` on subsequent turns.
831
+ "experimental.chat.messages.transform": async (_input, output) => {
765
832
  try {
766
- const providers = (cfg as { provider?: Record<string, unknown> } | undefined)?.provider;
767
- if (!providers || typeof providers !== "object") return;
768
- const debug = (msg: string) => ctx.logger.debug(`bizar: ${msg}`);
769
- for (const [name, provider] of Object.entries(providers)) {
770
- if (!provider || typeof provider !== "object") continue;
771
- const prov = provider as { options?: Record<string, unknown> };
772
- if (!prov.options || typeof prov.options !== "object") continue;
773
- const original = prov.options.fetch;
774
- if (typeof original !== "function") continue;
775
- // Only wrap once detect by stamping a sentinel.
776
- const wrapped = (original as { __bizarReasoningClean?: boolean })
777
- .__bizarReasoningClean;
778
- if (wrapped) continue;
779
- prov.options.fetch = wrapFetchForReasoningCleanup(
780
- original as Parameters<typeof wrapFetchForReasoningCleanup>[0],
781
- { debug, providers: [name] },
782
- );
783
- (prov.options.fetch as { __bizarReasoningClean?: boolean }).__bizarReasoningClean = true;
784
- debug(`wrapped provider.fetch for ${name}`);
833
+ const messages = (output as { messages?: unknown }).messages;
834
+ if (!Array.isArray(messages)) return;
835
+ for (const msg of messages) {
836
+ if (!msg || typeof msg !== "object") continue;
837
+ const m = msg as { role?: unknown; parts?: unknown };
838
+ if (m.role !== "assistant") continue;
839
+ if (!Array.isArray(m.parts)) continue;
840
+ for (const part of m.parts) {
841
+ if (!part || typeof part !== "object") continue;
842
+ const p = part as { type?: unknown; text?: unknown };
843
+ if (p.type === "text" && typeof p.text === "string" && p.text.includes("<think>")) {
844
+ p.text = stripInlineThinkBlocks(p.text);
845
+ }
846
+ }
785
847
  }
786
848
  } catch (err) {
787
849
  ctx.logger.warn(
788
- `bizar: config hook failed (passing through): ${
850
+ `bizar: messages.transform failed (passing through): ${
789
851
  err instanceof Error ? err.message : String(err)
790
852
  }`,
791
853
  );
@@ -1102,18 +1164,6 @@ function buildHooks(ctx: RuntimeContext, bg: BgDeps): Hooks {
1102
1164
  }
1103
1165
  },
1104
1166
 
1105
- // §3.1, §5.4 — handoff injection point. We push a single string onto
1106
- // `output.system` if a pending injection is queued for this session.
1107
- "experimental.chat.system.transform": async (input, output) => {
1108
- const sessionID = input.sessionID;
1109
- if (!sessionID) return;
1110
- const pending = ctx.pendingInjections.get(sessionID);
1111
- if (pending) {
1112
- output.system.push(pending);
1113
- ctx.pendingInjections.delete(sessionID);
1114
- }
1115
- },
1116
-
1117
1167
  // v0.4.2 — register the 4 background tools.
1118
1168
  tool: tools,
1119
1169
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar-plugin",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Bizar opencode plugin — loop detection, status reporting, handoff signal, background agents, and slash commands + visual plan flow for subagent activity",
5
5
  "type": "module",
6
6
  "main": "./index.ts",