@aibyzero/byz 0.1.4 → 0.1.6
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/CHANGELOG.md +13 -0
- package/README.md +33 -0
- package/dist/cli.js +20 -5
- package/dist/fast-session.js +276 -0
- package/dist/fast.js +9 -0
- package/dist/prewalk.js +150 -0
- package/dist/runtime/bundle/chunks/{chunk-QLXFMGH6.js → chunk-SKWR5IN4.js} +1 -1
- package/dist/runtime/bundle/cli.js +1 -1
- package/dist/runtime/bundle/index.js +1 -1
- package/dist/runtime/bundle/rpc-entry.js +1 -1
- package/dist/workflows.js +8 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.6 - 2026-08-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- 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)).
|
|
10
|
+
- 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)).
|
|
11
|
+
|
|
12
|
+
## 0.1.5 - 2026-08-28
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Made `byz workflow status` report the effective workflow when no target is given, distinguish active and available `none` states, and avoid unrelated workflow-root checks for `none` ([#20](https://github.com/kingxiaozhe/byz/pull/20)).
|
|
17
|
+
|
|
5
18
|
## 0.1.4 - 2026-08-27
|
|
6
19
|
|
|
7
20
|
### Added
|
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,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { prepareFastRuntimeArgs } from "./fast.js";
|
|
3
|
+
import { createFastSessionController, prepareFastRuntimeArgs, selectFastRuntimeArgs } from "./fast.js";
|
|
4
|
+
import { createPrewalkExtension } from "./prewalk.js";
|
|
4
5
|
import { main } from "./runtime/bundle/index.js";
|
|
5
6
|
import { handleByzUpdate } from "./update.js";
|
|
6
7
|
import { createWorkflowSwitchExtension, shouldEnableWorkflowSwitch, shouldLoadWorkflow } from "./workflow-switch.js";
|
|
@@ -28,21 +29,24 @@ try {
|
|
|
28
29
|
if (isRootHelp) {
|
|
29
30
|
console.error("BYZ updates: byz update (npm-managed global installations only)");
|
|
30
31
|
console.error("BYZ Fast: --fast (thinking=low; optional model: BYZ_FAST_MODEL)");
|
|
32
|
+
console.error("BYZ Prewalk: /prewalk (one-time handoff after the first successful workspace edit/write)");
|
|
31
33
|
console.error("BYZ workflows: --workflow <cm|cm-plugin|none> (default: BYZ_WORKFLOW or cm)");
|
|
32
|
-
console.error(
|
|
34
|
+
console.error(
|
|
35
|
+
"Commands: byz workflow list | byz workflow status [cm|cm-plugin|none] | byz workflow check <cm|cm-plugin>",
|
|
36
|
+
);
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
if (await handleWorkflowCommand(commandArgs)) {
|
|
39
|
+
if (await handleWorkflowCommand(commandArgs, { workflowId: parsedWorkflow.workflowId })) {
|
|
36
40
|
// BYZ-owned command handled without starting the Pi runtime.
|
|
37
41
|
} else if (await handleByzUpdate(commandArgs)) {
|
|
38
42
|
// BYZ release metadata and package target stay independent from Pi.
|
|
39
43
|
} else {
|
|
40
44
|
const loadWorkflow = shouldLoadWorkflow(commandArgs);
|
|
41
|
-
const runtimeArgs = loadWorkflow ? fastRuntime.args : fastRuntime.commandArgs;
|
|
42
45
|
const isInteractive = shouldEnableWorkflowSwitch(commandArgs, {
|
|
43
46
|
stdinIsTTY: process.stdin.isTTY,
|
|
44
47
|
stdoutIsTTY: process.stdout.isTTY,
|
|
45
48
|
});
|
|
49
|
+
const runtimeArgs = selectFastRuntimeArgs(fastRuntime, { isInteractive, loadWorkflow });
|
|
46
50
|
if (fastRuntime.enabled && loadWorkflow && isInteractive) {
|
|
47
51
|
console.error(
|
|
48
52
|
`BYZ Fast: model=${fastRuntime.model}, thinking=${fastRuntime.thinking}, workflow=${parsedWorkflow.workflowId}`,
|
|
@@ -58,7 +62,18 @@ try {
|
|
|
58
62
|
initialWorkflowId: parsedRuntimeWorkflow.workflowId,
|
|
59
63
|
resolveResources,
|
|
60
64
|
});
|
|
61
|
-
|
|
65
|
+
const fastController = createFastSessionController({
|
|
66
|
+
initiallyEnabled: fastRuntime.enabled,
|
|
67
|
+
initialUseConfiguredModel: fastRuntime.useConfiguredModel,
|
|
68
|
+
initialUseLowThinking: fastRuntime.useLowThinking,
|
|
69
|
+
});
|
|
70
|
+
const prewalkExtension = createPrewalkExtension({ fastController });
|
|
71
|
+
const byzExtension = (pi) => {
|
|
72
|
+
workflowExtension(pi);
|
|
73
|
+
fastController.extension(pi);
|
|
74
|
+
prewalkExtension(pi);
|
|
75
|
+
};
|
|
76
|
+
await main(parsedRuntimeWorkflow.forwardedArgs, { byzWorkflowExtensionFactory: byzExtension });
|
|
62
77
|
} else {
|
|
63
78
|
const prepared = await prepareWorkflowRuntimeArgs(runtimeArgs, { load: loadWorkflow });
|
|
64
79
|
await main(prepared.args);
|
|
@@ -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
|
+
}
|
package/dist/prewalk.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
const PREWALK_COMMAND_USAGE = "Usage: /prewalk [cancel|status]";
|
|
5
|
+
const WRITE_TOOL_NAMES = new Set(["edit", "write"]);
|
|
6
|
+
|
|
7
|
+
function hasBuiltinWriteTools(pi) {
|
|
8
|
+
const tools = new Map(pi.getAllTools().map((tool) => [tool.name, tool]));
|
|
9
|
+
return [...WRITE_TOOL_NAMES].every((name) => {
|
|
10
|
+
const sourceInfo = tools.get(name)?.sourceInfo;
|
|
11
|
+
return sourceInfo?.source === "builtin" && sourceInfo.path === `<builtin:${name}>`;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function isWorkspacePath(cwd, inputPath) {
|
|
16
|
+
if (typeof inputPath !== "string" || inputPath.length === 0) return false;
|
|
17
|
+
try {
|
|
18
|
+
const [workspaceRoot, targetPath] = await Promise.all([realpath(cwd), realpath(resolve(cwd, inputPath))]);
|
|
19
|
+
const relativePath = relative(workspaceRoot, targetPath);
|
|
20
|
+
return (
|
|
21
|
+
relativePath.length > 0 &&
|
|
22
|
+
relativePath !== ".." &&
|
|
23
|
+
!relativePath.startsWith(`..${sep}`) &&
|
|
24
|
+
!isAbsolute(relativePath)
|
|
25
|
+
);
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createPrewalkExtension({ fastController }) {
|
|
32
|
+
if (!fastController) throw new Error("Prewalk requires the Fast session controller.");
|
|
33
|
+
|
|
34
|
+
return function prewalkExtension(pi) {
|
|
35
|
+
let state = "idle";
|
|
36
|
+
let target;
|
|
37
|
+
let candidateQueue = Promise.resolve();
|
|
38
|
+
|
|
39
|
+
function reset() {
|
|
40
|
+
state = "idle";
|
|
41
|
+
target = undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function notifyStatus(ctx) {
|
|
45
|
+
if (state === "armed" && target) {
|
|
46
|
+
ctx.ui.notify(
|
|
47
|
+
`Prewalk: armed; target=${fastController.formatTarget(target)}; thinking=${target.thinking}`,
|
|
48
|
+
"info",
|
|
49
|
+
);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
ctx.ui.notify(`Prewalk: ${state === "switching" ? "switching" : "not armed"}.`, "info");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function cancel(ctx, message = "Prewalk: canceled.") {
|
|
56
|
+
if (state !== "armed") {
|
|
57
|
+
ctx.ui.notify("Prewalk: not armed.", "info");
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
reset();
|
|
61
|
+
ctx.ui.notify(message, "info");
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function arm(ctx) {
|
|
66
|
+
if (!ctx.isIdle()) {
|
|
67
|
+
ctx.ui.notify("Prewalk cannot arm while the agent is busy.", "warning");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!ctx.isProjectTrusted()) {
|
|
71
|
+
ctx.ui.notify("Prewalk requires a trusted project.", "warning");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (fastController.isActive()) {
|
|
75
|
+
ctx.ui.notify("Prewalk cannot arm while Fast is already on.", "warning");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (state === "armed") {
|
|
79
|
+
ctx.ui.notify("Prewalk is already armed.", "info");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!hasBuiltinWriteTools(pi)) {
|
|
83
|
+
ctx.ui.notify("Prewalk requires the built-in edit and write tools.", "warning");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const nextTarget = fastController.resolveTarget(ctx, { requireAuth: true, requireModel: true });
|
|
87
|
+
if (!nextTarget) return;
|
|
88
|
+
target = nextTarget;
|
|
89
|
+
state = "armed";
|
|
90
|
+
notifyStatus(ctx);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function considerToolResult(event, ctx) {
|
|
94
|
+
if (state !== "armed" || !target) return;
|
|
95
|
+
if (event.isError || !WRITE_TOOL_NAMES.has(event.toolName)) return;
|
|
96
|
+
if (!hasBuiltinWriteTools(pi)) return;
|
|
97
|
+
if (!(await isWorkspacePath(ctx.cwd, event.input?.path))) return;
|
|
98
|
+
if (state !== "armed" || !target) return;
|
|
99
|
+
|
|
100
|
+
const consumedTarget = target;
|
|
101
|
+
state = "switching";
|
|
102
|
+
target = undefined;
|
|
103
|
+
const applied = await fastController.applyTarget(ctx, consumedTarget);
|
|
104
|
+
reset();
|
|
105
|
+
if (applied) {
|
|
106
|
+
ctx.ui.notify(
|
|
107
|
+
`Prewalk: handed off to ${fastController.formatTarget(consumedTarget)}; thinking=${fastController.getThinkingLevel()}.`,
|
|
108
|
+
"info",
|
|
109
|
+
);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
ctx.ui.notify("Prewalk: handoff failed and was canceled.", "error");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
pi.registerCommand("prewalk", {
|
|
116
|
+
description: "Arm a one-time handoff to the Fast target after the first successful write",
|
|
117
|
+
handler: async (args, ctx) => {
|
|
118
|
+
const action = args.trim().toLowerCase();
|
|
119
|
+
if (!action) {
|
|
120
|
+
arm(ctx);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (action === "cancel") {
|
|
124
|
+
cancel(ctx);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (action === "status") {
|
|
128
|
+
notifyStatus(ctx);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
ctx.ui.notify(PREWALK_COMMAND_USAGE, "warning");
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
pi.on("tool_result", (event, ctx) => {
|
|
136
|
+
candidateQueue = candidateQueue.then(
|
|
137
|
+
() => considerToolResult(event, ctx),
|
|
138
|
+
() => considerToolResult(event, ctx),
|
|
139
|
+
);
|
|
140
|
+
return candidateQueue;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
fastController.onExplicitSelection((_event, ctx) => {
|
|
144
|
+
if (state === "armed") cancel(ctx, "Prewalk: canceled after an explicit model or thinking change.");
|
|
145
|
+
});
|
|
146
|
+
fastController.onActiveChange((active, ctx) => {
|
|
147
|
+
if (active && state === "armed") cancel(ctx, "Prewalk: canceled because Fast was enabled.");
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
}
|