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
1,100 changes: 141 additions & 959 deletions src/bundled/goose/goose.ts

Large diffs are not rendered by default.

58 changes: 29 additions & 29 deletions src/bundled/goose/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,28 @@ describe("GooseRuntime", () => {
expect(process?.cancelled).toEqual([process?.turns[0]?.sessionId]);
});

it("posts nothing goose wrote without `send`", async () => {
// Text the agent writes outside `send` is narration, not a reply.
for (const reply of ["I've replied to Will.", "Plain answer"]) {
const { deliver, published, runtime } = setup({ reply });
let activity: Promise<unknown> = Promise.resolve();
await deliver(message("1", "@Gander hi"), {
workingUntil: (done: Promise<unknown>) => {
activity = done;
},
});
expect(await activity, reply).toMatchObject({ ok: true, text: reply });
// The turn's report step ends by notifying, after any post it makes.
await new Promise<void>((resolve) => {
const stop = runtime.subscribe(() => {
stop();
resolve();
});
});
expect(published, reply).toEqual([]);
}
});

it("tells a subscriber when a new conversation's first turn starts working", async () => {
const { deliver, goose, runtime } = setup({ hold: true });
// As a view does: each notification reads the snapshot again.
Expand Down Expand Up @@ -337,7 +359,6 @@ describe("GooseRuntime", () => {
const { deliver, goose, published } = setup({
scope: "channel",
hold: true,
reply: "Forgot to send",
});
await deliver(reply("3", one));
await flush(100);
Expand All @@ -346,17 +367,14 @@ describe("GooseRuntime", () => {
const process = goose();
expect(process?.turns).toHaveLength(1);
expect(process?.turns[0]?.steered).toHaveLength(1);
process?.turns[0]?.finish();
process?.exit(1, "crashed\n");
await flush(100);
expect(published).toEqual([
{
kind: 9,
content: "Forgot to send",
tags: [
["h", channel],
["e", two.id, "", "reply"],
],
},
expect(published.map((event) => event.tags)).toEqual([
[
["h", channel],
["e", two.id, "", "reply"],
["p", alice],
],
]);
});

Expand All @@ -373,24 +391,6 @@ describe("GooseRuntime", () => {
expect(memoryOpen).not.toHaveBeenCalled();
});

it("posts goose's own text in the thread, or at a Solo chat's top level", async () => {
const plain = setup({ reply: "Plain answer" });
await plain.deliver(message("1", "@Gander hi"));
await flush(100);
expect(plain.published.map((event) => event.tags)).toEqual([
[
["h", channel],
["e", "1".repeat(64), "", "reply"],
],
]);
const solo = setup({ reply: "Plain answer", solo: new Set([channel]) });
await solo.deliver(message("1", "@Gander hi"));
await flush(100);
expect(solo.published.map((event) => event.tags)).toEqual([
[["h", channel]],
]);
});

it("leaves a Solo chat's session for goose to name, and offers that name", async () => {
const solo = setup({ hold: true, solo: new Set([channel]) });
const named = vi.fn();
Expand Down
17 changes: 3 additions & 14 deletions src/bundled/goose/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,7 @@ import {
} from "../claude-code/prompt";
import { ownerCanvas, readProject } from "../../features/agent-harness/project";
import type { ServeMcp, ToolServer } from "./goose";
import {
AgentSessions,
localSessions,
ServerPool,
SharedProcess,
} from "./sessions";
import { AgentSessions, localSessions, SharedProcess } from "./sessions";

export type Config = Readonly<{
/** A model the provider offers; empty uses the provider's default, or the
Expand Down Expand Up @@ -139,8 +134,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(
{
Expand Down Expand Up @@ -504,13 +497,10 @@ export class GooseRuntime {
.then((result) => {
if (result.undelivered)
agent.restoreNotes?.(channel, notes, noteRoot);
// Of the messages one turn answered, only one has its error or text.
// Of the messages one turn answered, only one has its error. An
// answer goose wrote without `send` stays in local activity.
if (result.error !== undefined)
return this.report(agent, channel, event, result.error, topLevel);
// An answer goose wrote without `send` is posted for it, so the
// thread is not left waiting.
if (result.text)
return this.post(agent, channel, event, result.text, topLevel);
})
.catch((error) => console.warn("goose turn failed", error))
.finally(() => this.notify());
Expand Down Expand Up @@ -765,7 +755,6 @@ export class GooseRuntime {
resolveDirectory: (path) => this.desktop.resolveDirectory(path),
tools,
process: this.goose,
pool: this.pool,
store: localSessions(this.storage, agent),
onChange: () => this.notify(),
// Read as each turn starts, so a change needs no restart.
Expand Down
4 changes: 3 additions & 1 deletion src/bundled/goose/sessions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,11 @@ describe("AgentSessions", () => {
const { fake, sessions } = setup({}, true);
const one = sessions.deliver("a", "one", 1);
const other = sessions.deliver("b", "other", 2);
await flush(20);
// Both turns are running before the interrupt.
await vi.waitFor(() => expect(fake.geese[0]?.turns).toHaveLength(2));
const [first, second] = fake.geese[0]?.turns ?? [];
expect(first?.text).toContain("one");
expect(second?.text).toContain("other");
await sessions.interrupt("a");
await flush();
expect(fake.geese[0]?.cancelled).toEqual([first?.sessionId]);
Expand Down
Loading
Loading