mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-25 10:42:45 +00:00
revert(acp): restore prior turn handling
This commit is contained in:
@@ -7,7 +7,6 @@ import type {
|
||||
AgentFeature,
|
||||
AgentModelDefinition,
|
||||
AgentMode,
|
||||
AgentSession,
|
||||
AgentSessionConfig,
|
||||
ProviderCatalog,
|
||||
} from "./agent-sdk-types.js";
|
||||
@@ -533,7 +532,6 @@ import {
|
||||
AGENT_PROVIDER_DEFINITIONS,
|
||||
buildProviderRegistry,
|
||||
createAllClients,
|
||||
wrapSessionProvider,
|
||||
} from "./provider-registry.js";
|
||||
import { FakeOmp } from "./providers/omp/test-utils/fake-omp.js";
|
||||
|
||||
@@ -1833,57 +1831,3 @@ describe("fetchCatalog", () => {
|
||||
expect(catalog.modes.map((mode) => mode.id)).toEqual(["ask"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("wrapSessionProvider", () => {
|
||||
function makeFakeSession(overrides?: Partial<AgentSession>): AgentSession {
|
||||
return {
|
||||
provider: "acp",
|
||||
id: "inner-session-id",
|
||||
capabilities: {},
|
||||
run: vi.fn(),
|
||||
startTurn: vi.fn(),
|
||||
subscribe: vi.fn(() => () => {}),
|
||||
streamHistory: (async function* () {})(),
|
||||
getRuntimeInfo: vi.fn(),
|
||||
getAvailableModes: vi.fn(),
|
||||
getCurrentMode: vi.fn(),
|
||||
setMode: vi.fn(),
|
||||
getPendingPermissions: vi.fn(() => []),
|
||||
respondToPermission: vi.fn(),
|
||||
describePersistence: vi.fn(() => null),
|
||||
interrupt: vi.fn(),
|
||||
close: vi.fn(),
|
||||
...overrides,
|
||||
} satisfies Partial<AgentSession> as AgentSession;
|
||||
}
|
||||
|
||||
// Any provider built on GenericACPAgentClient (cursor, kimi, kiro,
|
||||
// traecli, or a user-configured `extends: "acp"` entry) reports its own
|
||||
// `provider` as the base id ("acp"), which never matches the requested
|
||||
// label, so createResolvedProviderClient always routes it through this
|
||||
// wrapper. A forward missing here silently drops that capability for
|
||||
// every such provider, with no type error, since AgentSession declares
|
||||
// it optional.
|
||||
test("forwards steerActiveTurn to the inner session when present", async () => {
|
||||
const steerActiveTurn = vi.fn(async () => ({ status: "accepted" as const }));
|
||||
const inner = makeFakeSession({ steerActiveTurn });
|
||||
|
||||
const wrapped = wrapSessionProvider("cursor", inner);
|
||||
|
||||
expect(wrapped.steerActiveTurn).toBeDefined();
|
||||
const result = await wrapped.steerActiveTurn?.({ role: "user", content: [] } as never, {
|
||||
expectedTurnId: "turn-1",
|
||||
});
|
||||
expect(result).toEqual({ status: "accepted" });
|
||||
expect(steerActiveTurn).toHaveBeenCalledWith(
|
||||
{ role: "user", content: [] },
|
||||
{ expectedTurnId: "turn-1" },
|
||||
);
|
||||
});
|
||||
|
||||
test("leaves steerActiveTurn undefined when the inner session doesn't implement it", () => {
|
||||
const inner = makeFakeSession();
|
||||
const wrapped = wrapSessionProvider("cursor", inner);
|
||||
expect(wrapped.steerActiveTurn).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -461,7 +461,6 @@ export function wrapSessionProvider(provider: AgentProvider, inner: AgentSession
|
||||
close: () => inner.close(),
|
||||
listCommands: inner.listCommands?.bind(inner),
|
||||
setModel: inner.setModel?.bind(inner),
|
||||
steerActiveTurn: inner.steerActiveTurn?.bind(inner),
|
||||
setThinkingOption: inner.setThinkingOption?.bind(inner),
|
||||
setFeature: inner.setFeature?.bind(inner),
|
||||
revertConversation: inner.revertConversation?.bind(inner),
|
||||
|
||||
@@ -93,15 +93,6 @@ interface ACPSessionInternals {
|
||||
acpMcpServers(): unknown[];
|
||||
}
|
||||
|
||||
interface ACPSteerInternals {
|
||||
sessionId: string | null;
|
||||
connection: {
|
||||
extMethod: (method: string, params: unknown) => Promise<{ outcome: string }>;
|
||||
};
|
||||
steeringSupported: boolean;
|
||||
activeForegroundTurnId: string | null;
|
||||
}
|
||||
|
||||
interface ACPModelSelectionInternals {
|
||||
sessionId: string | null;
|
||||
connection: {
|
||||
@@ -157,117 +148,6 @@ function createSession(terminateProcess?: ProcessTerminator): ACPAgentSession {
|
||||
);
|
||||
}
|
||||
|
||||
describe("ACPAgentSession.steerActiveTurn", () => {
|
||||
function primeForSteer(
|
||||
session: ACPAgentSession,
|
||||
overrides: Partial<ACPSteerInternals> = {},
|
||||
): { internals: ACPSteerInternals; extMethod: ReturnType<typeof vi.fn> } {
|
||||
const extMethod = vi.fn(async () => ({ outcome: "injected" }));
|
||||
const internals = asInternals<ACPSteerInternals>(session);
|
||||
internals.sessionId = "session-1";
|
||||
internals.connection = { extMethod };
|
||||
internals.steeringSupported = true;
|
||||
internals.activeForegroundTurnId = "turn-1";
|
||||
Object.assign(internals, overrides);
|
||||
return { internals, extMethod };
|
||||
}
|
||||
|
||||
test("returns unavailable when the agent never advertised steering support", async () => {
|
||||
const session = createSession();
|
||||
primeForSteer(session, { steeringSupported: false });
|
||||
|
||||
const result = await session.steerActiveTurn("steer this", { expectedTurnId: "turn-1" });
|
||||
|
||||
expect(result).toEqual({ status: "unavailable" });
|
||||
});
|
||||
|
||||
test("returns unavailable when the caller's expected turn doesn't match the live one", async () => {
|
||||
// Guards against steering into the wrong turn: the caller's belief about
|
||||
// what's running raced with the agent's own turn boundary.
|
||||
const session = createSession();
|
||||
const { extMethod } = primeForSteer(session, { activeForegroundTurnId: "turn-2" });
|
||||
|
||||
const result = await session.steerActiveTurn("steer this", { expectedTurnId: "turn-1" });
|
||||
|
||||
expect(result).toEqual({ status: "unavailable" });
|
||||
expect(extMethod).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("returns unavailable when there is no live connection", async () => {
|
||||
const session = createSession();
|
||||
primeForSteer(session, { connection: undefined as never });
|
||||
|
||||
const result = await session.steerActiveTurn("steer this", { expectedTurnId: "turn-1" });
|
||||
|
||||
expect(result).toEqual({ status: "unavailable" });
|
||||
});
|
||||
|
||||
test("forwards to _session/steering with the session id and prompt content", async () => {
|
||||
const session = createSession();
|
||||
const { extMethod } = primeForSteer(session);
|
||||
|
||||
await session.steerActiveTurn("steer this", { expectedTurnId: "turn-1" });
|
||||
|
||||
expect(extMethod).toHaveBeenCalledWith("_session/steering", {
|
||||
sessionId: "session-1",
|
||||
prompt: [{ type: "text", text: "steer this" }],
|
||||
_meta: { steering: { idleBehavior: "promptRequired" } },
|
||||
});
|
||||
});
|
||||
|
||||
test.each([
|
||||
["injected", "accepted"],
|
||||
["startedNewTurn", "unavailable"],
|
||||
["promptRequired", "unavailable"],
|
||||
["failed", "unavailable"],
|
||||
] as const)("maps agent outcome %s to status %s", async (outcome, status) => {
|
||||
const session = createSession();
|
||||
const { extMethod } = primeForSteer(session);
|
||||
extMethod.mockResolvedValue({ outcome });
|
||||
|
||||
const result = await session.steerActiveTurn("steer this", { expectedTurnId: "turn-1" });
|
||||
|
||||
expect(result).toEqual({ status });
|
||||
});
|
||||
|
||||
test("denies pending permissions after the steer is accepted", async () => {
|
||||
const session = createSession();
|
||||
primeForSteer(session);
|
||||
const events: AgentStreamEvent[] = [];
|
||||
session.subscribe((event) => events.push(event));
|
||||
const permission = session.requestPermission({
|
||||
sessionId: "session-1",
|
||||
toolCall: {
|
||||
toolCallId: "tool-1",
|
||||
title: "Run command",
|
||||
kind: "execute",
|
||||
status: "pending",
|
||||
},
|
||||
options: [
|
||||
{ optionId: "allow-once", name: "Allow", kind: "allow_once" },
|
||||
{ optionId: "reject-once", name: "Reject", kind: "reject_once" },
|
||||
],
|
||||
} satisfies RequestPermissionRequest);
|
||||
await Promise.resolve();
|
||||
|
||||
await session.steerActiveTurn("steer this", {
|
||||
expectedTurnId: "turn-1",
|
||||
clearPendingPermissions: true,
|
||||
});
|
||||
|
||||
await expect(permission).resolves.toEqual({
|
||||
outcome: { outcome: "selected", optionId: "reject-once" },
|
||||
});
|
||||
expect(session.getPendingPermissions()).toEqual([]);
|
||||
expect(events).toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: "permission_resolved",
|
||||
resolution: expect.objectContaining({ behavior: "deny" }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Typed substitute for the real tree-kill terminator. Records which child
|
||||
// processes it was asked to terminate, so tests assert on observable state
|
||||
// instead of spying on the production function. In "deferred" mode the
|
||||
|
||||
@@ -94,8 +94,6 @@ import {
|
||||
type ProviderCatalog,
|
||||
type ResolveAgentCreateConfigInput,
|
||||
type ResolveAgentCreateConfigResult,
|
||||
type SteerActiveTurnOptions,
|
||||
type SteerResult,
|
||||
type ToolCallDetail,
|
||||
type ToolCallTimelineItem,
|
||||
} from "../agent-sdk-types.js";
|
||||
@@ -1456,7 +1454,6 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
||||
private readonly extensionCommandsParser?: ACPExtensionCommandsParser;
|
||||
private currentTurnUsage: AgentUsage | undefined;
|
||||
private activeForegroundTurnId: string | null = null;
|
||||
private steeringSupported = false;
|
||||
private fallbackAssistantMessageId: string | null = null;
|
||||
private closed = false;
|
||||
private historyPending = false;
|
||||
@@ -1507,8 +1504,6 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
||||
this.child = spawned.child;
|
||||
this.connection = spawned.connection;
|
||||
this.agentCapabilities = spawned.initialize.agentCapabilities ?? null;
|
||||
this.steeringSupported =
|
||||
readRecord(readRecord(spawned.initialize._meta)?.steering)?.supported === true;
|
||||
|
||||
const response = await this.runACPRequest(() =>
|
||||
this.connection!.newSession({
|
||||
@@ -1543,8 +1538,6 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
||||
this.child = spawned.child;
|
||||
this.connection = spawned.connection;
|
||||
this.agentCapabilities = spawned.initialize.agentCapabilities ?? null;
|
||||
this.steeringSupported =
|
||||
readRecord(readRecord(spawned.initialize._meta)?.steering)?.supported === true;
|
||||
this.sessionId = handle.sessionId;
|
||||
this.bootstrapThreadEventPending = true;
|
||||
|
||||
@@ -1659,45 +1652,6 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
||||
return { turnId };
|
||||
}
|
||||
|
||||
async steerActiveTurn(
|
||||
prompt: AgentPromptInput,
|
||||
options: SteerActiveTurnOptions,
|
||||
): Promise<SteerResult> {
|
||||
if (
|
||||
!this.connection ||
|
||||
!this.sessionId ||
|
||||
!this.steeringSupported ||
|
||||
this.activeForegroundTurnId !== options.expectedTurnId
|
||||
) {
|
||||
return { status: "unavailable" };
|
||||
}
|
||||
const response = await this.runACPRequest(() =>
|
||||
this.connection!.extMethod("_session/steering", {
|
||||
sessionId: this.sessionId,
|
||||
prompt: toACPContentBlocks(prompt),
|
||||
_meta: { steering: { idleBehavior: "promptRequired" } },
|
||||
}),
|
||||
);
|
||||
if (response.outcome !== "injected") {
|
||||
return { status: "unavailable" };
|
||||
}
|
||||
if (options.clearPendingPermissions) {
|
||||
await this.clearPendingPermissionsForSteer();
|
||||
}
|
||||
return { status: "accepted" };
|
||||
}
|
||||
|
||||
private async clearPendingPermissionsForSteer(): Promise<void> {
|
||||
const requestIds = Array.from(this.pendingPermissions.keys());
|
||||
for (const requestId of requestIds) {
|
||||
if (!this.pendingPermissions.has(requestId)) continue;
|
||||
await this.respondToPermission(requestId, {
|
||||
behavior: "deny",
|
||||
message: "The user answered with a message instead of approving. Their message follows.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(callback: (event: AgentStreamEvent) => void): () => void {
|
||||
this.subscribers.add(callback);
|
||||
if (this.sessionId) {
|
||||
|
||||
Reference in New Issue
Block a user