Merge PR #6820: fix(coding-agent): preserve parent todos in vibe mode (@Iron-Ham)
This commit is contained in:
+9
-6
@@ -2,9 +2,10 @@
|
||||
|
||||
Vibe mode turns the session into a **director** that drives persistent background
|
||||
worker sessions instead of editing code itself. In vibe mode your own toolset is
|
||||
stripped down to `read` plus five worker-control tools; the workers do the
|
||||
grepping, editing, running, and building, and you verify their work by reading
|
||||
the files they touch.
|
||||
stripped down to `read`, optional parent-owned `todo`, plus five worker-control
|
||||
tools; the workers do the grepping, editing, running, and building, and you
|
||||
verify their work by reading the files they touch. When available, `todo` is
|
||||
only the director's parent-session bookkeeping tool; workers do not own it.
|
||||
|
||||
## Enabling and disabling
|
||||
|
||||
@@ -16,8 +17,9 @@ Toggle it with the `/vibe` slash command:
|
||||
/vibe # run again to exit
|
||||
```
|
||||
|
||||
- Entering installs the vibe tools, reduces the active toolset to `read` + the
|
||||
vibe tools, and injects the director instructions for the turn.
|
||||
- Entering installs the vibe tools, reduces the active toolset to `read`,
|
||||
optional parent-owned `todo`, and the vibe tools, and injects the director
|
||||
instructions for the turn.
|
||||
- An inline prompt (`/vibe <prompt>`) enters the mode and submits that prompt as
|
||||
the first directive.
|
||||
- Exiting restores the previous toolset and **kills every worker session** — a
|
||||
@@ -62,7 +64,8 @@ concurrently is the normal shape.
|
||||
3. Keep directing other workers while turns are in flight; `vibe_wait` only when
|
||||
blocked.
|
||||
4. When a turn result arrives, `read` the touched files to verify claims before
|
||||
building on them, then `vibe_send` the next step.
|
||||
building on them. When the optional parent-owned `todo` tool is available,
|
||||
reconcile verified work through it, then `vibe_send` the next step.
|
||||
5. Route by difficulty: draft with `fast`, escalate to `good` when `fast` stalls
|
||||
or the problem needs judgment.
|
||||
6. `vibe_kill` finished or stuck workers; `vibe_list` to recover the roster.
|
||||
|
||||
@@ -160,6 +160,7 @@
|
||||
- Fixed `@czottmann/pi-automode` failing legacy extension validation because the pi-ai compatibility shim omitted `clampThinkingLevel`, then failing every classified tool call because `ctx.modelRegistry` omitted `getApiKeyAndHeaders`. ([#6648](https://github.com/can1357/oh-my-pi/issues/6648))
|
||||
- Fixed hide-secrets placeholders conflicting with hashline edit headers by replacing hash-delimited tokens with the unambiguous `$$HASH$$` format ([#6631](https://github.com/can1357/oh-my-pi/issues/6631)).
|
||||
- Fixed the advisor silently swallowing its own quarantined turns: when an advisor called an ungranted tool (e.g. `bash`) its whole turn was discarded before dispatch, so its advice never reached the primary agent and the failure surfaced only in advisor diagnostics — every other non-recovering failure branch notifies the host UI, but quarantine re-primed silently with no bound. A persistently-quarantining advisor now surfaces a `notifyFailure` warning in the main UI (deduped, cleared on the next successful turn) and stops the unbounded silent re-prime loop ([#6661](https://github.com/can1357/oh-my-pi/issues/6661)).
|
||||
- Fixed `/vibe` mode preventing the director from completing parent todos after verifying worker results: a registered built-in `todo` tool now remains active alongside `read` and Vibe controls, while normal worker sessions still do not receive it.
|
||||
- Fixed the Docker `natives-builder` stage failing to build releases ≥ 17.1.1: the native audio stack added bindgen (miniaudio needs libclang) and a bundled-opus CMake build (needs cmake + make), none of which were installed in the slim builder image.
|
||||
- Fixed a configured `modelRoles.default` naming an extension-registered model (listed in `enabledModels`) silently running on a different in-scope provider's model. The startup model scope is resolved before extensions call `registerProvider()`, so the default role dropped out of scope and `buildSessionOptions` pinned `options.model` to the first scoped model — which marked the model "explicit" and suppressed the post-extension default-role re-resolution. A configured default that can't be found in the startup scope is now deferred so it re-resolves against the fully registered, still `enabledModels`-scoped catalog once extensions load ([#6694](https://github.com/can1357/oh-my-pi/issues/6694)).
|
||||
- Fixed Parakeet speech-to-text failing to load `sherpa-onnx-node` from Windows source workspaces when Bun installed the wrapper under `packages/coding-agent/node_modules` but hoisted its native platform package to the repository root ([#6690](https://github.com/can1357/oh-my-pi/issues/6690)).
|
||||
|
||||
@@ -3277,9 +3277,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
|
||||
/**
|
||||
* `/vibe` toggle. Entering installs the ephemeral vibe tools, strips the
|
||||
* active toolset down to `read` plus those tools, and injects the director
|
||||
* context. Exiting unregisters them, restores the previous toolset, and kills
|
||||
* every worker session so workers cannot outlive the mode that directs them.
|
||||
* active toolset down to `read`, optional parent-owned `todo`, plus those
|
||||
* tools, and injects the director context. Exiting unregisters them, restores
|
||||
* the previous toolset, and kills every worker session so workers cannot
|
||||
* outlive the mode that directs them.
|
||||
*/
|
||||
async handleVibeModeCommand(initialPrompt?: string): Promise<void> {
|
||||
if (this.vibeModeEnabled) {
|
||||
@@ -3317,7 +3318,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
const ownerScope = vibeRegistry.ownerScope(this.#vibeParentSession());
|
||||
vibeRegistry.activateScope(ownerScope);
|
||||
const previousTools = this.session.getEnabledToolNames();
|
||||
await this.session.activateVibeTools(["read"]);
|
||||
const vibeBaseTools = ["read"];
|
||||
if (this.session.hasBuiltInTool("todo")) vibeBaseTools.push("todo");
|
||||
await this.session.activateVibeTools(vibeBaseTools);
|
||||
this.#vibeModePreviousTools = previousTools;
|
||||
this.#vibeModeOwnerScope = ownerScope;
|
||||
this.vibeModeEnabled = true;
|
||||
@@ -3330,7 +3333,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
}
|
||||
this.#updateVibeModeStatus();
|
||||
if (options?.persistModeChange !== false) this.sessionManager.appendModeChange("vibe");
|
||||
this.showStatus("Vibe mode enabled. You direct fast/good worker sessions; toolset is read + vibe tools.");
|
||||
this.showStatus(
|
||||
"Vibe mode enabled. You direct fast/good worker sessions; toolset is read + optional parent Todo + vibe tools.",
|
||||
);
|
||||
}
|
||||
|
||||
async #exitVibeMode(): Promise<void> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<vibe-mode>
|
||||
Vibe mode is ON. You are the DIRECTOR. You do not edit, run, grep, or build anything yourself — your hands are off the keyboard. You drive two kinds of worker CLIs, each a full coding agent with every normal tool, and you verify their work by reading files.
|
||||
|
||||
Your entire toolset: `read`, `vibe_spawn`, `vibe_send`, `vibe_wait`, `vibe_kill`, `vibe_list`.
|
||||
Your entire toolset: `read`{{#if todoAvailable}}, `todo`{{/if}}, `vibe_spawn`, `vibe_send`, `vibe_wait`, `vibe_kill`, `vibe_list`.
|
||||
|
||||
# The two CLIs you drive
|
||||
|
||||
@@ -16,6 +16,9 @@ Sessions are persistent conversations, like terminals you keep open. A session r
|
||||
2. `vibe_spawn` with a complete, self-contained brief: files, constraints, acceptance criteria. Workers start blank — they never see this conversation.
|
||||
3. Sends and spawns return immediately; results arrive on their own when a worker finishes its turn. Keep directing other sessions meanwhile; call `vibe_wait` only when you cannot proceed without a result.
|
||||
4. When a turn result arrives, judge it: `read` the touched files to verify claims before building on them. Follow up with `vibe_send` — corrections, next step, or a review request.
|
||||
{{#if todoAvailable}}
|
||||
After reading and verifying a worker result, use `todo` to maintain the parent session's list. Workers do not own this bookkeeping.
|
||||
{{/if}}
|
||||
5. Route by difficulty: draft with `fast`, escalate to `good` when `fast` stalls or the problem needs judgment; have `good` design and `fast` execute the mechanical parts.
|
||||
6. `vibe_kill` a session that is stuck or whose workstream is done; `vibe_list` when you lose track of the roster.
|
||||
|
||||
|
||||
@@ -4587,7 +4587,9 @@ export class AgentSession {
|
||||
return {
|
||||
role: "custom",
|
||||
customType: "vibe-mode-context",
|
||||
content: prompt.render(vibeModeActivePrompt),
|
||||
content: prompt.render(vibeModeActivePrompt, {
|
||||
todoAvailable: this.getActiveToolNames().includes("todo"),
|
||||
}),
|
||||
display: false,
|
||||
attribution: "agent",
|
||||
timestamp: Date.now(),
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* Contracts: /vibe mode toggle on InteractiveMode.
|
||||
*
|
||||
* 1. Vibe tools do not exist in the session registry before the mode is entered.
|
||||
* 2. Entering registers and activates exactly `read` plus the vibe tools.
|
||||
* 2. Entering registers and activates exactly `read`, parent-owned `todo`, plus
|
||||
* the vibe tools.
|
||||
* 3. Exiting unregisters the vibe tools and restores the pre-vibe active toolset
|
||||
* exactly, including the legitimate empty set.
|
||||
*/
|
||||
@@ -15,6 +16,7 @@ import { InteractiveMode } from "@oh-my-pi/pi-coding-agent/modes/interactive-mod
|
||||
import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
|
||||
import { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
|
||||
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
|
||||
import { normalizeCustomMessagePayload } from "@oh-my-pi/pi-coding-agent/session/messages";
|
||||
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
|
||||
import { FileSessionStorage, type WriteTextAtomicOptions } from "@oh-my-pi/pi-coding-agent/session/session-storage";
|
||||
import { VIBE_TOOL_NAMES } from "@oh-my-pi/pi-coding-agent/tools/vibe";
|
||||
@@ -85,6 +87,7 @@ describe("InteractiveMode vibe mode toggle", () => {
|
||||
let authStorage: AuthStorage;
|
||||
let session: AgentSession;
|
||||
let mode: InteractiveMode;
|
||||
let modelRegistry: ModelRegistry;
|
||||
let storage: ExitFaultStorage;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -97,12 +100,11 @@ describe("InteractiveMode vibe mode toggle", () => {
|
||||
tempDir = TempDir.createSync("@pi-vibe-toggle-");
|
||||
await Settings.init({ inMemory: true, cwd: tempDir.path() });
|
||||
authStorage = await AuthStorage.create(path.join(tempDir.path(), "testauth.db"));
|
||||
const modelRegistry = new ModelRegistry(authStorage);
|
||||
modelRegistry = new ModelRegistry(authStorage);
|
||||
const model = modelRegistry.find("anthropic", "claude-sonnet-4-5");
|
||||
if (!model) throw new Error("Expected claude-sonnet-4-5 to exist in registry");
|
||||
|
||||
const registryTools = [stubTool("read")];
|
||||
|
||||
const registryTools = [stubTool("read"), stubTool("todo")];
|
||||
storage = new ExitFaultStorage();
|
||||
session = new AgentSession({
|
||||
agent: new Agent({
|
||||
@@ -117,6 +119,7 @@ describe("InteractiveMode vibe mode toggle", () => {
|
||||
settings: Settings.isolated({}),
|
||||
modelRegistry,
|
||||
toolRegistry: new Map(registryTools.map(tool => [tool.name, tool])),
|
||||
builtInToolNames: registryTools.map(tool => tool.name),
|
||||
createVibeTools: () => VIBE_TOOL_NAMES.map(stubTool),
|
||||
});
|
||||
mode = new InteractiveMode(session, "test", undefined, undefined, undefined, undefined, new EventBus());
|
||||
@@ -132,29 +135,88 @@ describe("InteractiveMode vibe mode toggle", () => {
|
||||
resetSettingsForTest();
|
||||
});
|
||||
|
||||
it("restores the exact pre-vibe toolset on exit, including an empty one", async () => {
|
||||
expect(session.getAllToolNames()).toEqual(["read"]);
|
||||
it("preserves the parent Todo tool and restores the exact pre-vibe toolset on exit", async () => {
|
||||
expect(session.getAllToolNames().toSorted()).toEqual(["read", "todo"]);
|
||||
expect(session.getActiveToolNames()).toEqual([]);
|
||||
|
||||
await mode.handleVibeModeCommand();
|
||||
expect(mode.vibeModeEnabled).toBe(true);
|
||||
const inMode = session.getActiveToolNames();
|
||||
expect(inMode).toContain("read");
|
||||
expect(inMode).toContain("todo");
|
||||
for (const name of VIBE_TOOL_NAMES) {
|
||||
expect(inMode).toContain(name);
|
||||
}
|
||||
expect(inMode.toSorted()).toEqual(["read", ...VIBE_TOOL_NAMES].toSorted());
|
||||
expect(session.getAllToolNames().toSorted()).toEqual(["read", ...VIBE_TOOL_NAMES].toSorted());
|
||||
expect(inMode.toSorted()).toEqual(["read", "todo", ...VIBE_TOOL_NAMES].toSorted());
|
||||
expect(session.getAllToolNames().toSorted()).toEqual(["read", "todo", ...VIBE_TOOL_NAMES].toSorted());
|
||||
|
||||
// Toggle off: the empty previous toolset must come back — vibe tools
|
||||
// must not leak past the mode.
|
||||
const sendCustomMessage = vi.spyOn(session, "sendCustomMessage");
|
||||
await session.sendVibeModeContext({ deliverAs: "steer" });
|
||||
const message = normalizeCustomMessagePayload(sendCustomMessage.mock.calls[0]?.[0]);
|
||||
const content = typeof message.content === "string" ? message.content : "";
|
||||
expect(message.customType).toBe("vibe-mode-context");
|
||||
expect(content).toContain("`todo`");
|
||||
expect(content).toContain("parent session's list");
|
||||
expect(content).toContain("Workers do not own this bookkeeping.");
|
||||
|
||||
// Toggle off: the empty previous toolset must come back — only the
|
||||
// ephemeral vibe tools must leave the registry.
|
||||
await mode.handleVibeModeCommand();
|
||||
expect(mode.vibeModeEnabled).toBe(false);
|
||||
expect(session.getActiveToolNames()).toEqual([]);
|
||||
expect(session.getAllToolNames()).toEqual(["read"]);
|
||||
expect(session.getAllToolNames().toSorted()).toEqual(["read", "todo"]);
|
||||
});
|
||||
|
||||
it("preserves workers and mode metadata on a same-session reload", async () => {
|
||||
it("keeps a same-named non-built-in Todo tool unavailable in Vibe mode", async () => {
|
||||
const model = session.model;
|
||||
if (!model) throw new Error("Expected active model");
|
||||
const foreignTodoSession = new AgentSession({
|
||||
agent: new Agent({
|
||||
initialState: {
|
||||
model,
|
||||
systemPrompt: ["Test"],
|
||||
tools: [],
|
||||
messages: [],
|
||||
},
|
||||
}),
|
||||
sessionManager: SessionManager.create(tempDir.path(), tempDir.path()),
|
||||
settings: Settings.isolated({}),
|
||||
modelRegistry,
|
||||
toolRegistry: new Map(["read", "todo"].map(name => [name, stubTool(name)])),
|
||||
builtInToolNames: ["read"],
|
||||
createVibeTools: () => VIBE_TOOL_NAMES.map(stubTool),
|
||||
});
|
||||
const foreignTodoMode = new InteractiveMode(
|
||||
foreignTodoSession,
|
||||
"test",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
new EventBus(),
|
||||
);
|
||||
|
||||
try {
|
||||
await foreignTodoMode.handleVibeModeCommand();
|
||||
expect(foreignTodoSession.getActiveToolNames().toSorted()).toEqual(["read", ...VIBE_TOOL_NAMES].toSorted());
|
||||
|
||||
const sendCustomMessage = vi.spyOn(foreignTodoSession, "sendCustomMessage");
|
||||
await foreignTodoSession.sendVibeModeContext({ deliverAs: "steer" });
|
||||
const message = normalizeCustomMessagePayload(sendCustomMessage.mock.calls[0]?.[0]);
|
||||
const content = typeof message.content === "string" ? message.content : "";
|
||||
expect(content).not.toContain("`todo`");
|
||||
expect(content).not.toContain("parent session's list");
|
||||
|
||||
await foreignTodoMode.handleVibeModeCommand();
|
||||
expect(foreignTodoSession.getActiveToolNames()).toEqual([]);
|
||||
expect(foreignTodoSession.getAllToolNames().toSorted()).toEqual(["read", "todo"]);
|
||||
} finally {
|
||||
foreignTodoMode.stop();
|
||||
await foreignTodoSession.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves workers, Todo access, and mode metadata on a same-session reload", async () => {
|
||||
await mode.init({ suppressWelcomeIntro: true });
|
||||
await mode.handleVibeModeCommand();
|
||||
await session.sessionManager.ensureOnDisk();
|
||||
@@ -173,6 +235,13 @@ describe("InteractiveMode vibe mode toggle", () => {
|
||||
expect(await switching).toBe(true);
|
||||
|
||||
expect(mode.vibeModeEnabled).toBe(true);
|
||||
expect(session.getActiveToolNames()).toEqual(expect.arrayContaining(["read", "todo", ...VIBE_TOOL_NAMES]));
|
||||
const sendCustomMessage = vi.spyOn(session, "sendCustomMessage");
|
||||
await session.sendVibeModeContext({ deliverAs: "steer" });
|
||||
const message = normalizeCustomMessagePayload(sendCustomMessage.mock.calls[0]?.[0]);
|
||||
const content = typeof message.content === "string" ? message.content : "";
|
||||
expect(content).toContain("`todo`");
|
||||
expect(content).toContain("parent session's list");
|
||||
expect(suspend).toHaveBeenCalledTimes(1);
|
||||
expect(terminate).not.toHaveBeenCalled();
|
||||
expect(vibeModeEntryCount(session.sessionManager)).toBe(1);
|
||||
@@ -230,7 +299,7 @@ describe("InteractiveMode vibe mode toggle", () => {
|
||||
it("does not clobber the target's active tools with the source snapshot when switching out of vibe", async () => {
|
||||
await mode.init({ suppressWelcomeIntro: true });
|
||||
// Pre-vibe snapshot on the source session is empty; entering vibe activates
|
||||
// read + the vibe tools.
|
||||
// read, parent-owned todo, and the vibe tools.
|
||||
await mode.handleVibeModeCommand();
|
||||
expect(mode.vibeModeEnabled).toBe(true);
|
||||
expect(session.getActiveToolNames()).toContain("read");
|
||||
@@ -246,9 +315,10 @@ describe("InteractiveMode vibe mode toggle", () => {
|
||||
expect(await session.switchSession(targetFile)).toBe(true);
|
||||
|
||||
expect(mode.vibeModeEnabled).toBe(false);
|
||||
// The transient vibe tools are gone, but the genuinely-active `read` tool
|
||||
// must survive — the source's empty pre-vibe snapshot must not wipe it.
|
||||
expect(session.getActiveToolNames()).toEqual(["read"]);
|
||||
// The transient vibe tools are gone, but the genuinely-active `read` and
|
||||
// parent-owned `todo` tools must survive — the source's empty pre-vibe
|
||||
// snapshot must not wipe them.
|
||||
expect(session.getActiveToolNames()).toEqual(["read", "todo"]);
|
||||
for (const name of VIBE_TOOL_NAMES) {
|
||||
expect(session.getActiveToolNames()).not.toContain(name);
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ describe("createAgentSession defaultInactive tool activation", () => {
|
||||
await session.dispose();
|
||||
}
|
||||
});
|
||||
it("registers vibe tools only during explicit vibe activation", async () => {
|
||||
it("registers vibe tools only during explicit vibe activation and exposes parent Todo bookkeeping", async () => {
|
||||
const tempDir = makeTempDir();
|
||||
const { session } = await createAgentSession(baseOptions(tempDir));
|
||||
const previousActiveToolNames = session.getActiveToolNames();
|
||||
@@ -315,12 +315,27 @@ describe("createAgentSession defaultInactive tool activation", () => {
|
||||
expect(session.getToolByName(name)).toBeUndefined();
|
||||
}
|
||||
|
||||
await session.activateVibeTools(["read"]);
|
||||
await session.activateVibeTools(["read", "todo"]);
|
||||
const todo = session.getToolByName("todo");
|
||||
if (!todo) throw new Error("Expected real Todo tool");
|
||||
expect(session.getActiveToolNames()).toContain("todo");
|
||||
for (const name of VIBE_TOOL_NAMES) {
|
||||
expect(session.getToolByName(name)).toBeDefined();
|
||||
expect(session.getActiveToolNames()).toContain(name);
|
||||
}
|
||||
|
||||
await todo.execute("vibe-todo-init", {
|
||||
op: "init",
|
||||
list: [{ phase: "Work", items: ["Worker change"] }],
|
||||
});
|
||||
await todo.execute("vibe-todo-done", { op: "done", task: "Worker change" });
|
||||
expect(session.getTodoPhases()).toMatchObject([
|
||||
{
|
||||
name: "Work",
|
||||
tasks: [{ content: "Worker change", status: "completed" }],
|
||||
},
|
||||
]);
|
||||
|
||||
await session.deactivateVibeTools(previousActiveToolNames);
|
||||
for (const name of VIBE_TOOL_NAMES) {
|
||||
expect(session.getToolByName(name)).toBeUndefined();
|
||||
@@ -331,6 +346,55 @@ describe("createAgentSession defaultInactive tool activation", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("rehydrates completed parent Todo work from persisted session history", async () => {
|
||||
const tempDir = makeTempDir();
|
||||
const sessionManager = SessionManager.create(tempDir, tempDir);
|
||||
const { session } = await createAgentSession({
|
||||
...baseOptions(tempDir),
|
||||
sessionManager,
|
||||
});
|
||||
|
||||
try {
|
||||
await session.activateVibeTools(["read", "todo"]);
|
||||
const todo = session.getToolByName("todo");
|
||||
if (!todo) throw new Error("Expected real Todo tool");
|
||||
const init = await todo.execute("vibe-todo-init", {
|
||||
op: "init",
|
||||
list: [{ phase: "Worker flow", items: ["Reconcile worker result"] }],
|
||||
});
|
||||
const done = await todo.execute("vibe-todo-done", { op: "done", task: "Reconcile worker result" });
|
||||
for (const [toolCallId, result] of [
|
||||
["vibe-todo-init", init],
|
||||
["vibe-todo-done", done],
|
||||
] as const) {
|
||||
sessionManager.appendMessage({
|
||||
role: "toolResult",
|
||||
toolCallId,
|
||||
toolName: "todo",
|
||||
content: result.content,
|
||||
details: result.details,
|
||||
isError: result.isError === true,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
await sessionManager.ensureOnDisk();
|
||||
const sessionFile = session.sessionFile;
|
||||
if (!sessionFile) throw new Error("Expected persisted session file");
|
||||
|
||||
session.setTodoPhases([]);
|
||||
expect(session.getTodoPhases()).toEqual([]);
|
||||
expect(await session.switchSession(sessionFile)).toBe(true);
|
||||
expect(session.getTodoPhases()).toMatchObject([
|
||||
{
|
||||
name: "Worker flow",
|
||||
tasks: [{ content: "Reconcile worker result", status: "completed" }],
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
await session.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not register the xAI TTS tool unless enabled", async () => {
|
||||
const tempDir = makeTempDir();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user