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
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,16 @@ Sessions are written as JSONL event logs under `.fledgling/sessions` by default.

The agent advertises ACP `loadSession` support. Loading a session rebuilds the user/assistant message history from stored events and replays that transcript to the ACP client.

## ACP and MCP capability contract

Fledgling uses ACP for agent orchestration: initialization, sessions, prompt turns, cancellation, session updates, and session loading.

Workspace capabilities are MCP-first. File reads, file writes, directory listing, text search, and command execution are exposed through MCP tools, including the `firstPartyWorkspace` MCP server. Fledgling does not currently use ACP host filesystem or terminal APIs as workspace backends.

ACP host `readTextFile`, `writeTextFile`, and terminal integration may be added later for hosts that only expose those APIs. Permission prompts for MCP-backed writes and commands are also future work.

## Current Limitations

- Prompt input is treated mostly as text; rich ACP prompt parts are not yet preserved semantically.
- Workspace access is provided through MCP tools, not ACP host filesystem or terminal APIs.
- ACP authentication, session modes, model selection, and permission prompts are still minimal or stubbed.
- Tool result context hints are recorded but not yet used for full context-store replay.
69 changes: 69 additions & 0 deletions packages/acp-agent/src/agent-prompt-cancellation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ describe("FledglingAgent prompt cancellation", () => {
}
});

it("advertises only the minimum supported ACP agent capabilities", async () => {
const { agent } = await createTestAgent();

const response = await agent.initialize({ protocolVersion: 1, clientCapabilities: {} } as never);

expect(typeof response.protocolVersion).toBe("number");
expect(response.agentCapabilities).toEqual({
loadSession: true,
mcpCapabilities: {
http: true,
sse: true
}
});
});

it("streams text responses and persists user and assistant messages", async () => {
const { agent, sessionId, streamText, sessionFile, sessionUpdates } = await createTestAgent();
streamText.mockReturnValueOnce(createImmediateStream([{ type: "text-delta", text: "hello" }]));
Expand Down Expand Up @@ -115,6 +130,42 @@ describe("FledglingAgent prompt cancellation", () => {
);
});

it("does not use ACP host filesystem or permission methods during prompt flow", async () => {
const { manager: sessionManager, sessionFile, tempDir: createdTempDir } = await createTempSessionManager();
const sessionUpdates: FakeSessionUpdate[] = [];
const hostMethods = createFailingHostMethods();
const streamText = vi.fn(() => createImmediateStream([{ type: "text-delta", text: "mcp-first" }]));
const { FledglingAgent } = await import("./agent.js");
const agent = new FledglingAgent(
{
...createFakeConnection(sessionUpdates),
...hostMethods
} as never,
{
toolProvider: {
createSessionTools: vi.fn(async () => ({ clients: [], tools: {} }))
},
sessionManager,
modelTurnRunner: {
runModelTurn: streamText
}
} satisfies FledglingAgentDependencies
);
const session = await agent.newSession({ cwd: createdTempDir, mcpServers: [] });

await expect(agent.prompt({ sessionId: session.sessionId, prompt: [{ type: "text", text: "hi" }] })).resolves.toEqual({
stopReason: "end_turn"
});

expect(hostMethods.requestPermission).not.toHaveBeenCalled();
expect(hostMethods.readTextFile).not.toHaveBeenCalled();
expect(hostMethods.writeTextFile).not.toHaveBeenCalled();
expect(await loadStoredMessages(sessionFile)).toEqual([
["message.user", "hi"],
["message.assistant", "mcp-first"]
]);
});

it("normalizes model start failures into diagnostics and durable errors", async () => {
const { agent, sessionId, streamText, sessionFile, sessionUpdates } = await createTestAgent();
streamText.mockImplementationOnce(() => {
Expand Down Expand Up @@ -426,6 +477,24 @@ function createFakeConnection(sessionUpdates: FakeSessionUpdate[] = []): { sessi
};
}

function createFailingHostMethods(): {
readonly requestPermission: ReturnType<typeof vi.fn>;
readonly readTextFile: ReturnType<typeof vi.fn>;
readonly writeTextFile: ReturnType<typeof vi.fn>;
} {
return {
requestPermission: vi.fn(async () => {
throw new Error("ACP host permission should not be used");
}),
readTextFile: vi.fn(async () => {
throw new Error("ACP host readTextFile should not be used");
}),
writeTextFile: vi.fn(async () => {
throw new Error("ACP host writeTextFile should not be used");
})
};
}

async function createTempSessionManager(): Promise<{
readonly manager: FileSystemSessionManager;
readonly sessionFile: string;
Expand Down
4 changes: 4 additions & 0 deletions packages/acp-agent/tools/smoke-client.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,18 @@ class SmokeClient {
}

async requestPermission() {
// The smoke host implements the ACP client surface, but Fledgling's current
// workspace path is MCP-first and should not call these host methods.
return { outcome: { outcome: "cancelled" } };
}

async writeTextFile() {
// Inert ACP filesystem stub; workspace writes are provided by MCP tools.
return {};
}

async readTextFile() {
// Inert ACP filesystem stub; workspace reads are provided by MCP tools.
return { content: "" };
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/acp-host-log/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,20 @@ class LoggingHost implements acp.Client {
}

public async requestPermission(params: acp.RequestPermissionRequest): Promise<acp.RequestPermissionResponse> {
// This host implements the ACP client surface for logging, but Fledgling's
// current workspace path is MCP-first and should not call this method.
emitTranscript("session/request_permission", { params });
return { outcome: { outcome: "cancelled" } };
}

public async writeTextFile(params: acp.WriteTextFileRequest): Promise<acp.WriteTextFileResponse> {
// Inert ACP filesystem stub; workspace writes are provided by MCP tools.
emitTranscript("fs/write_text_file", { params });
return {};
}

public async readTextFile(params: acp.ReadTextFileRequest): Promise<acp.ReadTextFileResponse> {
// Inert ACP filesystem stub; workspace reads are provided by MCP tools.
emitTranscript("fs/read_text_file", { params });
return { content: "" };
}
Expand Down
4 changes: 4 additions & 0 deletions packages/web-demo/src/acp-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@ class DemoClient {
}

public async requestPermission(): Promise<acp.RequestPermissionResponse> {
// The demo client implements the ACP client surface, but workspace access in
// this app is provided by browser MCP workspace tools.
return { outcome: { outcome: "cancelled" } };
}

public async writeTextFile(): Promise<acp.WriteTextFileResponse> {
// Inert ACP filesystem stub; workspace writes are provided by MCP tools.
return {};
}

public async readTextFile(): Promise<acp.ReadTextFileResponse> {
// Inert ACP filesystem stub; workspace reads are provided by MCP tools.
return { content: "" };
}
}
Expand Down
Loading