@aibyzero/byz 0.1.5 → 0.1.7

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 (33) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +33 -0
  3. package/dist/cli.js +18 -18
  4. package/dist/conversation/conversation-extension.js +85 -0
  5. package/dist/conversation/interaction-policy.js +53 -0
  6. package/dist/conversation/routing-policy.js +102 -0
  7. package/dist/fast-session.js +276 -0
  8. package/dist/fast.js +9 -0
  9. package/dist/prewalk.js +150 -0
  10. package/dist/runtime/bundle/chunks/{chunk-WMLOZBQV.js → chunk-JDLZZASY.js} +7 -7
  11. package/dist/runtime/bundle/cli.js +1 -1
  12. package/dist/runtime/bundle/index.js +1 -1
  13. package/dist/runtime/bundle/rpc-entry.js +1 -1
  14. package/dist/runtime/config.d.ts.map +1 -1
  15. package/dist/runtime/config.js +3 -3
  16. package/dist/runtime/config.js.map +1 -1
  17. package/dist/runtime/core/extensions/index.d.ts +1 -1
  18. package/dist/runtime/core/extensions/index.d.ts.map +1 -1
  19. package/dist/runtime/core/extensions/index.js.map +1 -1
  20. package/dist/runtime/core/extensions/runner.d.ts.map +1 -1
  21. package/dist/runtime/core/extensions/runner.js +3 -0
  22. package/dist/runtime/core/extensions/runner.js.map +1 -1
  23. package/dist/runtime/core/extensions/types.d.ts +17 -0
  24. package/dist/runtime/core/extensions/types.d.ts.map +1 -1
  25. package/dist/runtime/core/extensions/types.js.map +1 -1
  26. package/dist/runtime/modes/interactive/interactive-mode.d.ts +5 -0
  27. package/dist/runtime/modes/interactive/interactive-mode.d.ts.map +1 -1
  28. package/dist/runtime/modes/interactive/interactive-mode.js +59 -6
  29. package/dist/runtime/modes/interactive/interactive-mode.js.map +1 -1
  30. package/dist/runtime/modes/rpc/rpc-mode.d.ts.map +1 -1
  31. package/dist/runtime/modes/rpc/rpc-mode.js +9 -0
  32. package/dist/runtime/modes/rpc/rpc-mode.js.map +1 -1
  33. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.7 - 2026-08-29
6
+
7
+ ### Added
8
+
9
+ - Added same-session routing and collaboration preferences that classify common requests, inject minimal per-turn guidance, and show route details only on demand.
10
+ - Added the default BYZ conversation shell with a goal-first welcome, low-noise progress, on-demand details, and natural-language confirmation input.
11
+
12
+ ### Changed
13
+
14
+ - Changed the default interactive shell to hide internal resources, tool rows, model metadata, and advanced controls until requested.
15
+
16
+ ## 0.1.6 - 2026-08-28
17
+
18
+ ### Added
19
+
20
+ - Added same-session `/fast` hot switching with reversible model and thinking state, explicit user choices taking priority, and no changes to the active workflow or conversation ([#22](https://github.com/kingxiaozhe/byz/pull/22)).
21
+ - Added opt-in `/prewalk` one-time handoff after the first successful built-in workspace edit or write, reusing the authenticated Fast target without changing the conversation or workflow ([#23](https://github.com/kingxiaozhe/byz/pull/23)).
22
+
5
23
  ## 0.1.5 - 2026-08-28
6
24
 
7
25
  ### Fixed
package/README.md CHANGED
@@ -56,6 +56,39 @@ An explicit `--model` or `--thinking` option always wins. Continuing or resuming
56
56
  an existing session keeps that session's model and applies the Fast thinking
57
57
  default. Normal `byz` runs ignore `BYZ_FAST_MODEL` and remain unchanged.
58
58
 
59
+ Inside an interactive session, Fast can be changed without restarting BYZ or
60
+ starting a new conversation:
61
+
62
+ ```text
63
+ /fast
64
+ /fast on
65
+ /fast off
66
+ /fast status
67
+ ```
68
+
69
+ `/fast on` snapshots the current model and thinking, then applies the same Fast
70
+ defaults. `/fast off` restores that snapshot. The active workflow, conversation,
71
+ session, skills, prompts, and tools do not change. Explicitly selecting a model
72
+ or thinking level exits Fast and keeps that explicit choice. BYZ rejects Fast
73
+ state changes while the agent is running, and an unavailable or unauthenticated
74
+ configured model leaves the current state unchanged.
75
+
76
+ ## Prewalk
77
+
78
+ Arm a one-time handoff when the current model should understand the task and perform the first successful workspace edit before Fast continues:
79
+
80
+ ```text
81
+ /prewalk
82
+ /prewalk status
83
+ /prewalk cancel
84
+ ```
85
+
86
+ `/prewalk` is available only in an interactive, trusted, idle session. It resolves and authenticates the same target used by Fast before arming. If `BYZ_FAST_MODEL` is unset, the current authenticated model remains selected and only thinking changes to `low` after the handoff.
87
+
88
+ Only the first successful Pi built-in `edit` or `write` whose real target remains inside the current workspace consumes the armed state. Read-only tools, failed writes, extension tools with the same name, and file or directory symlink escapes do not trigger it. Parallel tool results are checked serially and can consume the state only once.
89
+
90
+ Prewalk preserves the current conversation, session, workflow, skills, prompts, and tools. It does not add another model call for planning. An explicit model or thinking selection cancels an armed Prewalk and keeps the user's choice. Enabling Fast also cancels it; Prewalk refuses to arm when Fast is already active.
91
+
59
92
  ## Workflows
60
93
 
61
94
  ```bash
package/dist/cli.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { prepareFastRuntimeArgs } from "./fast.js";
3
+ import { createConversationExtension } from "./conversation/conversation-extension.js";
4
+ import { createFastSessionController, prepareFastRuntimeArgs, selectFastRuntimeArgs } from "./fast.js";
5
+ import { createPrewalkExtension } from "./prewalk.js";
4
6
  import { main } from "./runtime/bundle/index.js";
5
7
  import { handleByzUpdate } from "./update.js";
6
8
  import { createWorkflowSwitchExtension, shouldEnableWorkflowSwitch, shouldLoadWorkflow } from "./workflow-switch.js";
@@ -24,15 +26,6 @@ try {
24
26
  const fastRuntime = prepareFastRuntimeArgs(args);
25
27
  const parsedWorkflow = parseWorkflowOption(fastRuntime.commandArgs);
26
28
  const commandArgs = parsedWorkflow.forwardedArgs;
27
- const isRootHelp = commandArgs.length === 1 && (commandArgs[0] === "--help" || commandArgs[0] === "-h");
28
- if (isRootHelp) {
29
- console.error("BYZ updates: byz update (npm-managed global installations only)");
30
- console.error("BYZ Fast: --fast (thinking=low; optional model: BYZ_FAST_MODEL)");
31
- console.error("BYZ workflows: --workflow <cm|cm-plugin|none> (default: BYZ_WORKFLOW or cm)");
32
- console.error(
33
- "Commands: byz workflow list | byz workflow status [cm|cm-plugin|none] | byz workflow check <cm|cm-plugin>",
34
- );
35
- }
36
29
 
37
30
  if (await handleWorkflowCommand(commandArgs, { workflowId: parsedWorkflow.workflowId })) {
38
31
  // BYZ-owned command handled without starting the Pi runtime.
@@ -40,17 +33,11 @@ try {
40
33
  // BYZ release metadata and package target stay independent from Pi.
41
34
  } else {
42
35
  const loadWorkflow = shouldLoadWorkflow(commandArgs);
43
- const runtimeArgs = loadWorkflow ? fastRuntime.args : fastRuntime.commandArgs;
44
36
  const isInteractive = shouldEnableWorkflowSwitch(commandArgs, {
45
37
  stdinIsTTY: process.stdin.isTTY,
46
38
  stdoutIsTTY: process.stdout.isTTY,
47
39
  });
48
- if (fastRuntime.enabled && loadWorkflow && isInteractive) {
49
- console.error(
50
- `BYZ Fast: model=${fastRuntime.model}, thinking=${fastRuntime.thinking}, workflow=${parsedWorkflow.workflowId}`,
51
- );
52
- }
53
-
40
+ const runtimeArgs = selectFastRuntimeArgs(fastRuntime, { isInteractive, loadWorkflow });
54
41
  if (loadWorkflow && isInteractive) {
55
42
  const parsedRuntimeWorkflow = parseWorkflowOption(runtimeArgs);
56
43
  const resolveResources = (workflowId) =>
@@ -60,7 +47,20 @@ try {
60
47
  initialWorkflowId: parsedRuntimeWorkflow.workflowId,
61
48
  resolveResources,
62
49
  });
63
- await main(parsedRuntimeWorkflow.forwardedArgs, { byzWorkflowExtensionFactory: workflowExtension });
50
+ const fastController = createFastSessionController({
51
+ initiallyEnabled: fastRuntime.enabled,
52
+ initialUseConfiguredModel: fastRuntime.useConfiguredModel,
53
+ initialUseLowThinking: fastRuntime.useLowThinking,
54
+ });
55
+ const prewalkExtension = createPrewalkExtension({ fastController });
56
+ const conversationExtension = createConversationExtension();
57
+ const byzExtension = (pi) => {
58
+ conversationExtension(pi);
59
+ workflowExtension(pi);
60
+ fastController.extension(pi);
61
+ prewalkExtension(pi);
62
+ };
63
+ await main(parsedRuntimeWorkflow.forwardedArgs, { byzWorkflowExtensionFactory: byzExtension });
64
64
  } else {
65
65
  const prepared = await prepareWorkflowRuntimeArgs(runtimeArgs, { load: loadWorkflow });
66
66
  await main(prepared.args);
@@ -0,0 +1,85 @@
1
+ import { createInteractionPolicy, formatDecision, parseConversationControl } from "./interaction-policy.js";
2
+ import { createRoutingPolicy } from "./routing-policy.js";
3
+
4
+ const WELCOME = "BYZ\n\n你想让我帮你做什么?";
5
+
6
+ export function createConversationExtension() {
7
+ const policy = createInteractionPolicy();
8
+ const routingPolicy = createRoutingPolicy();
9
+
10
+ return function conversationExtension(pi) {
11
+ let progressTimer;
12
+
13
+ function clearProgressTimer() {
14
+ if (progressTimer) clearTimeout(progressTimer);
15
+ progressTimer = undefined;
16
+ }
17
+
18
+ pi.on("session_start", (_event, ctx) => {
19
+ routingPolicy.reset();
20
+ ctx.ui.setTitle?.("BYZ");
21
+ ctx.ui.setMessagePresenter?.((message) => policy.presentAssistantMessage(message));
22
+ ctx.ui.setToolExecutionVisible?.(false);
23
+ ctx.ui.setFooter?.(() => ({
24
+ invalidate() {},
25
+ render() {
26
+ return [];
27
+ },
28
+ }));
29
+ ctx.ui.setConfirmationPresenter?.(async ({ title, message, confirm }) => {
30
+ const prompt = formatDecision({
31
+ impact: message,
32
+ recommendation: "确认",
33
+ alternative: "取消",
34
+ onReject: "不会执行此操作",
35
+ });
36
+ const answer = await ctx.ui.input(prompt, `${title}:输入“确认”或“取消”`);
37
+ const choice = answer ? parseConversationControl(answer) : undefined;
38
+ if (choice === "accept" || choice === "proceed") return true;
39
+ if (choice === "reject") return false;
40
+ return confirm();
41
+ });
42
+ ctx.ui.notify(WELCOME, "info");
43
+ });
44
+ pi.on("agent_start", (_event, ctx) => {
45
+ policy.resetProgress();
46
+ clearProgressTimer();
47
+ progressTimer = setTimeout(() => {
48
+ const message = policy.present("progress", "");
49
+ if (message) ctx.ui.setWorkingMessage?.(message);
50
+ }, 30_000);
51
+ });
52
+ pi.on("agent_end", () => {
53
+ clearProgressTimer();
54
+ });
55
+ pi.on("session_shutdown", () => {
56
+ routingPolicy.reset();
57
+ clearProgressTimer();
58
+ });
59
+ function showDetails(ctx) {
60
+ policy.setDetailEnabled(true);
61
+ ctx.ui.setToolExecutionVisible?.(true);
62
+ ctx.ui.notify("已展开细节。高级控制:/fast、/prewalk、/workflow。", "info");
63
+ }
64
+
65
+ pi.registerCommand("details", {
66
+ description: "Show BYZ advanced controls",
67
+ handler: async (_args, ctx) => showDetails(ctx),
68
+ });
69
+ pi.on("before_agent_start", async (event, ctx) => {
70
+ const route = routingPolicy.route(event.prompt);
71
+ if (route.details || parseConversationControl(event.prompt) === "detail") showDetails(ctx);
72
+ if (policy.isDetailEnabled()) {
73
+ ctx.ui.notify(
74
+ `当前类别:${route.kind}。当前偏好:主动程度 ${route.preferences.autonomy},交付 ${route.preferences.delivery}。`,
75
+ "info",
76
+ );
77
+ }
78
+ return {
79
+ systemPrompt: `${event.systemPrompt ?? ""}\n\nBYZ collaboration guidance for this turn:\n${route.instructions}`,
80
+ };
81
+ });
82
+ };
83
+ }
84
+
85
+ export { WELCOME };
@@ -0,0 +1,53 @@
1
+ const INTERNAL_TERMS = /\b(?:model|skill|workflow|fast|prewalk|token|tool|step\s*\d+)\b/gi;
2
+
3
+ export function createInteractionPolicy() {
4
+ let progressShown = false;
5
+ let detailEnabled = false;
6
+
7
+ function present(kind, message, { detail = false } = {}) {
8
+ if (kind === "progress") {
9
+ if (progressShown) return undefined;
10
+ progressShown = true;
11
+ return "正在处理,稍后给你结果。";
12
+ }
13
+ if (kind === "advanced-control" && !detailEnabled && !detail) return undefined;
14
+ if (detail || detailEnabled) return message;
15
+ return message.replace(INTERNAL_TERMS, "内部设置");
16
+ }
17
+
18
+ function presentAssistantMessage(message) {
19
+ if (detailEnabled) return message;
20
+ const content = message.content
21
+ .filter((part) => part.type === "text")
22
+ .map((part) => ({ ...part, text: part.text.replace(INTERNAL_TERMS, "内部设置") }));
23
+ return { ...message, content };
24
+ }
25
+
26
+ return {
27
+ present,
28
+ presentAssistantMessage,
29
+ setDetailEnabled(enabled) {
30
+ detailEnabled = enabled;
31
+ },
32
+ isDetailEnabled() {
33
+ return detailEnabled;
34
+ },
35
+ resetProgress() {
36
+ progressShown = false;
37
+ },
38
+ };
39
+ }
40
+
41
+ export function parseConversationControl(input) {
42
+ const value = input.trim();
43
+ if (/^(展开细节|查看细节|显示细节)$/.test(value)) return "detail";
44
+ if (/^(少问一点|直接做)$/.test(value)) return "proceed";
45
+ if (/^(关键动作先问我|先问我)$/.test(value)) return "confirm";
46
+ if (/^(取消|不要|拒绝)$/.test(value)) return "reject";
47
+ if (/^(确认|继续|同意|可以)$/.test(value)) return "accept";
48
+ return undefined;
49
+ }
50
+
51
+ export function formatDecision({ impact, recommendation, alternative, onReject }) {
52
+ return `需要你决定\n影响:${impact}\n建议:${recommendation}\n其他选择:${alternative}\n如果拒绝:${onReject}`;
53
+ }
@@ -0,0 +1,102 @@
1
+ const DEFAULT_PREFERENCES = Object.freeze({
2
+ autonomy: "balanced",
3
+ delivery: "normal",
4
+ });
5
+
6
+ const CONTROL_PATTERNS = [
7
+ [/关键动作先问我|先问我/, { autonomy: "confirm-key-actions" }],
8
+ [/少问一点/, { autonomy: "fewer-questions" }],
9
+ [/直接做/, { autonomy: "direct" }],
10
+ [/先给三个方向/, { delivery: "three-directions" }],
11
+ [/展开细节|查看细节|显示细节/, {}],
12
+ ];
13
+
14
+ const FALLBACKS = Object.freeze({
15
+ research: {
16
+ missingInput: "需要可访问链接、正文、关键词或目标来源。",
17
+ fallback: "可先基于你提供的摘要做初步判断,或请你补充正文、截图、链接。",
18
+ },
19
+ "bug-fix": {
20
+ missingInput: "需要复现步骤、报错信息、相关输入或运行环境。",
21
+ fallback: "可先根据现象列排查清单,等你补充日志后再定位。",
22
+ },
23
+ "project-recovery": {
24
+ missingInput: "需要当前项目路径、上次进度、任务状态或错误现场。",
25
+ fallback: "可先读取本地项目状态并汇总可恢复线索。",
26
+ },
27
+ general: {
28
+ missingInput: "需要更明确的目标、输入材料或期望输出。",
29
+ fallback: "可先给出可选方向或最小可执行下一步。",
30
+ },
31
+ });
32
+
33
+ function classify(goal) {
34
+ if (/https?:\/\/|链接|帖子|查一下|调研|研究/.test(goal)) return "research";
35
+ if (/三个方向|创意|设计|文案|写作|起个名字/.test(goal)) return "creative";
36
+ if (/报错|bug|缺陷|无法复现|修复/.test(goal)) return "bug-fix";
37
+ if (/新功能|实现|开发|增加.*功能|添加.*功能/.test(goal)) return "feature";
38
+ if (/继续.*项目|恢复.*项目|上次.*停/.test(goal)) return "project-recovery";
39
+ return "general";
40
+ }
41
+
42
+ function instructionsFor(kind, preferences) {
43
+ const collaboration = [
44
+ preferences.autonomy === "direct" ? "在安全且可逆的范围内直接推进,不要先要求用户选择内部模式。" : undefined,
45
+ preferences.autonomy === "fewer-questions" ? "仅在缺少会明显改变结果的关键信息时提一个问题。" : undefined,
46
+ preferences.autonomy === "confirm-key-actions" ? "关键动作前说明影响并请求确认。" : undefined,
47
+ preferences.delivery === "three-directions" ? "先给出三个可区分的方向,再建议一个推荐方向。" : undefined,
48
+ ].filter(Boolean);
49
+ const task = {
50
+ research: "优先说明来源是否可访问;无法取得内容时说明缺失并建议用户提供正文或替代来源。",
51
+ creative: "先给出可见的创作骨架或方向,避免暴露内部能力名称。",
52
+ "bug-fix": "先以可复现证据定位;无法复现时说明缺少的环境、输入或日志。",
53
+ feature: "先确认目标与范围;涉及高影响动作时保持既有确认边界。",
54
+ "project-recovery": "只在当前会话可见信息足够时恢复上下文;否则说明需要的项目状态。",
55
+ general: "直接用自然语言处理目标;资料不足时说明未完成部分和可行替代路径。",
56
+ }[kind];
57
+ return [...collaboration, task].join("\n");
58
+ }
59
+
60
+ export function classifyRequest(prompt, preferences = DEFAULT_PREFERENCES) {
61
+ const kind = classify(prompt.trim());
62
+ return {
63
+ fallback: FALLBACKS[kind]?.fallback,
64
+ instructions: instructionsFor(kind, preferences),
65
+ kind,
66
+ missingInput: FALLBACKS[kind]?.missingInput,
67
+ };
68
+ }
69
+
70
+ export function parseSessionPreference(input) {
71
+ const preferences = {};
72
+ const details = /展开细节|查看细节|显示细节/.test(input);
73
+ let goal = input;
74
+ for (const [pattern, changes] of CONTROL_PATTERNS) {
75
+ if (pattern.test(goal)) Object.assign(preferences, changes);
76
+ goal = goal.replace(pattern, "");
77
+ }
78
+ return { details, goal: goal.replace(/[,,。;;]+/g, " ").trim(), preferences };
79
+ }
80
+
81
+ export const parseSessionPreferences = parseSessionPreference;
82
+
83
+ export function createRoutingPolicy() {
84
+ let preferences = { ...DEFAULT_PREFERENCES };
85
+
86
+ return {
87
+ route(input) {
88
+ const parsed = parseSessionPreference(input);
89
+ preferences = { ...preferences, ...parsed.preferences };
90
+ const route = classifyRequest(parsed.goal, preferences);
91
+ return {
92
+ ...route,
93
+ details: parsed.details,
94
+ goal: parsed.goal,
95
+ preferences: { ...preferences },
96
+ };
97
+ },
98
+ reset() {
99
+ preferences = { ...DEFAULT_PREFERENCES };
100
+ },
101
+ };
102
+ }
@@ -0,0 +1,276 @@
1
+ const FAST_COMMAND_USAGE = "Usage: /fast [on|off|status]";
2
+
3
+ function parseFastModelReference(value) {
4
+ const separatorIndex = value.indexOf("/");
5
+ if (separatorIndex <= 0 || separatorIndex === value.length - 1) return undefined;
6
+ return {
7
+ provider: value.slice(0, separatorIndex),
8
+ modelId: value.slice(separatorIndex + 1),
9
+ };
10
+ }
11
+
12
+ function formatModel(model) {
13
+ return model ? `${model.provider}/${model.id}` : "none";
14
+ }
15
+
16
+ function modelsMatch(left, right) {
17
+ return left?.provider === right?.provider && left?.id === right?.id;
18
+ }
19
+
20
+ export function createFastSessionController({
21
+ env = process.env,
22
+ initiallyEnabled = false,
23
+ initialUseConfiguredModel = true,
24
+ initialUseLowThinking = true,
25
+ } = {}) {
26
+ let active = false;
27
+ let snapshot;
28
+ let internalTransition = false;
29
+ let currentThinkingTransition;
30
+ let pi;
31
+ const ignoredThinkingTransitions = [];
32
+ const activeListeners = new Set();
33
+ const explicitSelectionListeners = new Set();
34
+
35
+ function notifyStatus(ctx) {
36
+ ctx.ui.notify(
37
+ `Fast: ${active ? "on" : "off"}; model=${formatModel(ctx.model)}; thinking=${pi.getThinkingLevel()}`,
38
+ "info",
39
+ );
40
+ }
41
+
42
+ async function emitListeners(listeners, event, ctx) {
43
+ for (const listener of listeners) await listener(event, ctx);
44
+ }
45
+
46
+ async function exitForExplicitSelection(event, ctx) {
47
+ if (internalTransition) return;
48
+ if (active) {
49
+ active = false;
50
+ snapshot = undefined;
51
+ await emitListeners(activeListeners, false, ctx);
52
+ }
53
+ await emitListeners(explicitSelectionListeners, event, ctx);
54
+ }
55
+
56
+ async function handleThinkingSelection(event, ctx) {
57
+ if (
58
+ currentThinkingTransition &&
59
+ event.previousLevel === currentThinkingTransition.previousLevel &&
60
+ event.level === pi.getThinkingLevel()
61
+ ) {
62
+ currentThinkingTransition.consumed = true;
63
+ return;
64
+ }
65
+ const ignoredIndex = ignoredThinkingTransitions.findIndex(
66
+ (transition) => transition.previousLevel === event.previousLevel && transition.level === event.level,
67
+ );
68
+ if (ignoredIndex !== -1) {
69
+ ignoredThinkingTransitions.splice(ignoredIndex, 1);
70
+ return;
71
+ }
72
+ await exitForExplicitSelection(event, ctx);
73
+ }
74
+
75
+ function beginThinkingTransition() {
76
+ const transition = {
77
+ previousLevel: pi.getThinkingLevel(),
78
+ level: pi.getThinkingLevel(),
79
+ consumed: false,
80
+ };
81
+ currentThinkingTransition = transition;
82
+ return transition;
83
+ }
84
+
85
+ function finishThinkingTransition(transition) {
86
+ transition.level = pi.getThinkingLevel();
87
+ currentThinkingTransition = undefined;
88
+ if (!transition.consumed && transition.level !== transition.previousLevel) {
89
+ ignoredThinkingTransitions.push(transition);
90
+ }
91
+ }
92
+
93
+ function setThinkingInternally(level) {
94
+ if (pi.getThinkingLevel() === level) return;
95
+ const transition = beginThinkingTransition();
96
+ try {
97
+ pi.setThinkingLevel(level);
98
+ } finally {
99
+ finishThinkingTransition(transition);
100
+ }
101
+ }
102
+
103
+ async function setModelInternally(model) {
104
+ const transition = beginThinkingTransition();
105
+ try {
106
+ return await pi.setModel(model);
107
+ } finally {
108
+ finishThinkingTransition(transition);
109
+ }
110
+ }
111
+
112
+ function resolveTarget(
113
+ ctx,
114
+ { requireAuth = false, requireModel = false, useConfiguredModel = true, useLowThinking = true } = {},
115
+ ) {
116
+ const configuredModel = useConfiguredModel ? env.BYZ_FAST_MODEL?.trim() : undefined;
117
+ let model = ctx.model;
118
+ if (configuredModel) {
119
+ const reference = parseFastModelReference(configuredModel);
120
+ if (!reference) {
121
+ ctx.ui.notify(`Invalid BYZ_FAST_MODEL "${configuredModel}". Expected provider/model.`, "error");
122
+ return undefined;
123
+ }
124
+ model = ctx.modelRegistry.find(reference.provider, reference.modelId);
125
+ if (!model) {
126
+ ctx.ui.notify(`Fast model "${configuredModel}" was not found.`, "error");
127
+ return undefined;
128
+ }
129
+ if (!ctx.modelRegistry.hasConfiguredAuth(model)) {
130
+ ctx.ui.notify(`Fast model "${configuredModel}" has no configured authentication.`, "error");
131
+ return undefined;
132
+ }
133
+ if (!ctx.model) {
134
+ ctx.ui.notify("Fast cannot preserve the current model because no model is selected.", "error");
135
+ return undefined;
136
+ }
137
+ }
138
+ if (!model && requireModel) {
139
+ ctx.ui.notify("Fast cannot preserve the current model because no model is selected.", "error");
140
+ return undefined;
141
+ }
142
+ if (model && requireAuth && !ctx.modelRegistry.hasConfiguredAuth(model)) {
143
+ ctx.ui.notify(`Fast model "${formatModel(model)}" has no configured authentication.`, "error");
144
+ return undefined;
145
+ }
146
+ return {
147
+ configuredModel,
148
+ model,
149
+ thinking: useLowThinking ? "low" : pi.getThinkingLevel(),
150
+ };
151
+ }
152
+
153
+ async function applyTarget(ctx, target) {
154
+ internalTransition = true;
155
+ try {
156
+ if (target.model && !modelsMatch(ctx.model, target.model)) {
157
+ const changed = await setModelInternally(target.model);
158
+ if (!changed) {
159
+ ctx.ui.notify(`Fast model "${formatModel(target.model)}" has no configured authentication.`, "error");
160
+ return false;
161
+ }
162
+ }
163
+ setThinkingInternally(target.thinking);
164
+ return true;
165
+ } catch (error) {
166
+ const message = error instanceof Error ? error.message : String(error);
167
+ ctx.ui.notify(`Fast target could not be applied: ${message}`, "error");
168
+ return false;
169
+ } finally {
170
+ internalTransition = false;
171
+ }
172
+ }
173
+
174
+ async function enable(ctx, { useConfiguredModel = true, useLowThinking = true } = {}) {
175
+ if (!ctx.isIdle()) {
176
+ ctx.ui.notify("Fast cannot change while the agent is busy.", "warning");
177
+ return;
178
+ }
179
+ if (active) {
180
+ notifyStatus(ctx);
181
+ return;
182
+ }
183
+
184
+ const target = resolveTarget(ctx, { useConfiguredModel, useLowThinking });
185
+ if (!target) return;
186
+ const nextSnapshot = {
187
+ model: ctx.model,
188
+ thinking: pi.getThinkingLevel(),
189
+ };
190
+ if (!(await applyTarget(ctx, target))) return;
191
+ snapshot = nextSnapshot;
192
+ active = true;
193
+ await emitListeners(activeListeners, true, ctx);
194
+ notifyStatus(ctx);
195
+ }
196
+
197
+ async function disable(ctx) {
198
+ if (!ctx.isIdle()) {
199
+ ctx.ui.notify("Fast cannot change while the agent is busy.", "warning");
200
+ return;
201
+ }
202
+ if (!active || !snapshot) {
203
+ notifyStatus(ctx);
204
+ return;
205
+ }
206
+
207
+ const originalTarget = { model: snapshot.model, thinking: snapshot.thinking };
208
+ if (!(await applyTarget(ctx, originalTarget))) return;
209
+ active = false;
210
+ snapshot = undefined;
211
+ await emitListeners(activeListeners, false, ctx);
212
+ notifyStatus(ctx);
213
+ }
214
+
215
+ function extension(extensionApi) {
216
+ pi = extensionApi;
217
+
218
+ pi.registerCommand("fast", {
219
+ description: "Switch Fast mode for the current session",
220
+ handler: async (args, ctx) => {
221
+ const action = args.trim().toLowerCase() || "status";
222
+ if (action === "status") {
223
+ notifyStatus(ctx);
224
+ return;
225
+ }
226
+ if (action === "on") {
227
+ await enable(ctx);
228
+ return;
229
+ }
230
+ if (action === "off") {
231
+ await disable(ctx);
232
+ return;
233
+ }
234
+ ctx.ui.notify(FAST_COMMAND_USAGE, "warning");
235
+ },
236
+ });
237
+
238
+ pi.on("model_select", exitForExplicitSelection);
239
+ pi.on("thinking_level_select", handleThinkingSelection);
240
+ if (initiallyEnabled) {
241
+ pi.on("session_start", async (_event, ctx) => {
242
+ await enable(ctx, {
243
+ useConfiguredModel: initialUseConfiguredModel,
244
+ useLowThinking: initialUseLowThinking,
245
+ });
246
+ });
247
+ }
248
+ }
249
+
250
+ return {
251
+ applyTarget,
252
+ extension,
253
+ formatTarget(target) {
254
+ return formatModel(target.model);
255
+ },
256
+ getThinkingLevel() {
257
+ return pi.getThinkingLevel();
258
+ },
259
+ isActive() {
260
+ return active;
261
+ },
262
+ onActiveChange(listener) {
263
+ activeListeners.add(listener);
264
+ return () => activeListeners.delete(listener);
265
+ },
266
+ onExplicitSelection(listener) {
267
+ explicitSelectionListeners.add(listener);
268
+ return () => explicitSelectionListeners.delete(listener);
269
+ },
270
+ resolveTarget,
271
+ };
272
+ }
273
+
274
+ export function createFastSwitchExtension(options) {
275
+ return createFastSessionController(options).extension;
276
+ }
package/dist/fast.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { getActiveByzOptionIndexes } from "./workflow-switch.js";
2
2
 
3
+ export { createFastSessionController, createFastSwitchExtension } from "./fast-session.js";
4
+
3
5
  const SESSION_OPTIONS = new Set(["--continue", "-c", "--resume", "-r", "--session", "--session-id", "--fork"]);
4
6
  const THINKING_SUFFIX_PATTERN = /:(off|minimal|low|medium|high|xhigh|max)$/;
5
7
 
@@ -63,5 +65,12 @@ export function prepareFastRuntimeArgs(args, env = process.env) {
63
65
  enabled: true,
64
66
  model: explicitModel ?? (resumesSession ? "session" : configuredModel || "default"),
65
67
  thinking: explicitThinking ?? modelThinking ?? "low",
68
+ useConfiguredModel: !hasModelOption && !resumesSession,
69
+ useLowThinking: !hasThinkingOption && !modelThinking,
66
70
  };
67
71
  }
72
+
73
+ export function selectFastRuntimeArgs(fastRuntime, { isInteractive, loadWorkflow }) {
74
+ if (isInteractive) return fastRuntime.commandArgs;
75
+ return loadWorkflow ? fastRuntime.args : fastRuntime.commandArgs;
76
+ }