Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/bundled/goose/goose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export const GOOSE: AcpHooks = {
program: "goose",
missing: "goose-acp is missing from this Buzz build.",
env: ENV,
// goose is asked to close every session, advertised or not.
closeSession: "always",
newSessionMeta: (buzz, { title, provider }) => ({
// The session's whole tool set: goose's developer tools and the Buzz
// tools, so extensions in the owner's goose config and plugins never load
Expand Down
5 changes: 1 addition & 4 deletions src/bundled/goose/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
turnPrompt,
} from "../claude-code/prompt";
import type { ServeMcp, Spawn, ToolServer } from "./goose";
import { AgentSessions, ServerPool, SharedProcess } from "./sessions";
import { AgentSessions, SharedProcess } from "./sessions";

export type Config = Readonly<{
/** A model the provider offers; empty uses the provider's default, or the
Expand Down Expand Up @@ -108,8 +108,6 @@ type Entry = {

export class GooseRuntime {
private readonly agents = new Map<string, Entry>();
/** The MCP servers every agent's sessions share. */
private readonly pool = new ServerPool();
/** The goose every agent's sessions open in. */
private readonly goose = new SharedProcess(
(id, options) => this.spawn(id, options),
Expand Down Expand Up @@ -502,7 +500,6 @@ export class GooseRuntime {
spawn: (id, options) => this.spawn(id, options),
tools,
process: this.goose,
pool: this.pool,
onChange: () => this.notify(),
// Read as each turn starts, so a change needs no restart.
choice: () => ({
Expand Down
26 changes: 16 additions & 10 deletions src/features/agent-harness/acp/agent-sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
// conversation's session is closed; its next message opens a new one, shown
// the conversation again. A one-off turn (a watch or timer wake) gets a
// session of its own, closed when it ends. Every session holds one of the
// host's MCP servers, in a pool the plugin's agents share, and the least
// recently used idle session closes to make room. Nothing is saved: sessions
// host's MCP servers, in one pool every ACP plugin's agents share, and the
// least recently used idle session closes to make room, whichever plugin's. Nothing is saved: sessions
// live as long as the app does.
import type { AcpProcess } from "./process";
import type { AcpSession } from "./session";
Expand All @@ -18,8 +18,8 @@ import type {

/** A conversation's session closes after this long without a message. */
export const IDLE_MS = 15 * 60_000;
/** The most MCP servers a pool's sessions hold at once, below the host's 64 for
* every plugin, so other plugins keep room. */
/** The most MCP servers the pool's sessions hold at once, below the host's 64
* for every plugin, so plugins not on this client keep room. */
export const POOL_LIMIT = 48;

/** A session's hold on one MCP server in the pool. */
Expand All @@ -30,8 +30,7 @@ export type Slot = {
evict(): Promise<unknown>;
};

/** The MCP servers a plugin's agents' sessions hold, least recently used
* first. One runtime shares one. */
/** The MCP servers agents' sessions hold, least recently used first. */
export class ServerPool {
private readonly slots = new Set<Slot>();
constructor(private readonly limit = POOL_LIMIT) {}
Expand Down Expand Up @@ -71,6 +70,9 @@ export class ServerPool {
}
}

/** The pool every ACP plugin's sessions share. */
export const SHARED_POOL = new ServerPool();

/** What a session is opened with. */
export type Launch = Readonly<{
/** The agent's working directory, absolute or `~/…`. */
Expand All @@ -86,8 +88,8 @@ export type SessionsOptions = Readonly<{
tools: ToolServer;
/** The process its sessions open in, started when needed. */
process: Readonly<{ get(): Promise<AcpProcess> }>;
/** The servers its plugin's sessions hold. */
pool: ServerPool;
/** The servers its sessions hold: the shared pool unless given. */
pool?: ServerPool;
/** The agent's current settings, for a new session. */
launch(): Promise<Launch>;
/** The agent's provider and model now, which each new turn runs on. */
Expand Down Expand Up @@ -309,6 +311,9 @@ export class AgentSessions {
for (const live of this.once.values()) if (live) void this.close(live);
}

private get pool() {
return this.options.pool ?? SHARED_POOL;
}
private stale(live: Live) {
return live.fingerprint !== this.options.fingerprint();
}
Expand All @@ -320,7 +325,8 @@ export class AgentSessions {
title: string | undefined,
use: (live: Live) => unknown,
): Promise<Live> {
const { pool, spawn } = this.options;
const { spawn } = this.options;
const { pool } = this;
// Read before `launch` reads the settings, so it never describes newer ones.
const fingerprint = this.options.fingerprint();
const launch = await this.options.launch();
Expand Down Expand Up @@ -408,7 +414,7 @@ export class AgentSessions {
}
/** Notes what the session has been shown and restarts its idle countdown. */
private touch(key: string, live: Live, seen?: number) {
this.options.pool.touch(live.slot);
this.pool.touch(live.slot);
clearTimeout(live.timer);
live.timer = setTimeout(() => {
if (live.session.busy) this.touch(key, live);
Expand Down
Loading
Loading