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
3 changes: 2 additions & 1 deletion gateways/web/app/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,8 @@ export const ChatView: Component<Props> = (props) => {

async function handleAbort(): Promise<void> {
try {
await session.abort();
const turn = await session.getCurrentTurn();
await turn?.abort();
} catch {
/* turn may have ended */
}
Expand Down
3 changes: 1 addition & 2 deletions gateways/web/test/mocks/piccolo-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ export function createEventObservable(events: AgentEvent[]): IObservable<AgentEv

function emptyTurn(): ITurn {
return {
getObservable: vi.fn().mockResolvedValue(createEventObservable([])),
getCallback: vi.fn().mockResolvedValue(undefined),
abort: vi.fn().mockResolvedValue(undefined),
} as unknown as ITurn;
}

Expand All @@ -60,7 +60,6 @@ export function createMockSession(
sendUserMessage: vi.fn().mockResolvedValue(undefined),
steer: vi.fn().mockResolvedValue(undefined),
followUp: vi.fn().mockResolvedValue(undefined),
abort: vi.fn().mockResolvedValue(undefined),
subscribe: vi.fn().mockResolvedValue(noopSubscription),
getCurrentTurn: vi.fn().mockResolvedValue(undefined),
getModel: vi.fn().mockResolvedValue(DEFAULT_MODEL),
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ export interface IObservable<T> {
*/
export interface ITurn {
getCallback(): Promise<IGatewayCallback | undefined>;
abort(): Promise<void>;
}

// ─── Context / Compaction ─────────────────────────────────────────────────────
Expand Down Expand Up @@ -326,7 +327,6 @@ export interface ISession extends IObservable<AgentEvent> {
sendUserMessage(content: string): Promise<void>;
steer(text: string): Promise<void>;
followUp(text: string): Promise<void>;
abort(): Promise<void>;
getCurrentTurn(): Promise<ITurn | undefined>;

// ─── Model management ────────────────────────────────────────────────────────
Expand Down
19 changes: 12 additions & 7 deletions packages/core/src/agent-session-do.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,6 @@ export class SessionTarget extends RpcTarget implements ISession {
followUp(text: string): Promise<void> {
return this.#do.followUp(text);
}
abort(): Promise<void> {
return this.#do.abort();
}
getCurrentTurn(): Promise<ITurn | undefined> {
return this.#do.getCurrentTurn();
}
Expand Down Expand Up @@ -376,9 +373,11 @@ export class AgentSessionDO extends DurableObject<Env> implements ISession {
// concurrent caller that runs before the microtask queue yields will
// immediately see #currentTurn !== null and throw.
if (this.#currentTurn !== null) {
throw new Error("A turn is already in progress. Call abort() before starting a new turn.");
throw new Error(
"A turn is already in progress. Call getCurrentTurn() and abort() that turn before starting a new turn.",
);
}
const turn = new TurnImpl(callback);
const turn = new TurnImpl(callback, () => this.#abortCurrentTurn());
this.#currentTurn = turn;

try {
Expand Down Expand Up @@ -480,7 +479,7 @@ export class AgentSessionDO extends DurableObject<Env> implements ISession {
this.#followUpQueue.push(text);
}

async abort(): Promise<void> {
async #abortCurrentTurn(): Promise<void> {
this.#agentAbortController?.abort();
}

Expand Down Expand Up @@ -1161,15 +1160,21 @@ export class AgentSessionDO extends DurableObject<Env> implements ISession {

export class TurnImpl extends RpcTarget implements ITurn {
readonly #callback: IGatewayCallback | undefined;
readonly #abort: () => Promise<void>;

constructor(callback: IGatewayCallback | undefined) {
constructor(callback: IGatewayCallback | undefined, abort: () => Promise<void>) {
super();
this.#callback = callback;
this.#abort = abort;
}

async getCallback(): Promise<IGatewayCallback | undefined> {
return this.#callback;
}

async abort(): Promise<void> {
await this.#abort();
}
}

// ─── Token estimation ─────────────────────────────────────────────────────────
Expand Down
6 changes: 3 additions & 3 deletions packages/core/test/do/agent-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ describe("AgentSessionDO — abort", () => {
await instance._init(sid, "user-1");
const flushed = waitForEvent(instance, (e) => e.type === "turn_flushed");
const ev = await drainTurn(instance, "go");
await instance.abort();
await (await instance.getCurrentTurn())?.abort();
await flushed;
return ev;
});
Expand Down Expand Up @@ -857,7 +857,7 @@ describe("AgentSessionDO — abort() (inlined)", () => {
await instance._init(sid, "user-1");
const flushed = waitForEvent(instance, (e) => e.type === "turn_flushed");
const promptPromise = drainTurn(instance, "go");
await instance.abort();
await (await instance.getCurrentTurn())?.abort();
const ev = await promptPromise;
await flushed;
return ev;
Expand All @@ -875,7 +875,7 @@ describe("AgentSessionDO — abort() (inlined)", () => {
await instance._init(sid, "user-1");
const flushed = waitForEvent(instance, (e) => e.type === "turn_flushed");
const promptPromise = drainTurn(instance, "go");
await instance.abort();
await (await instance.getCurrentTurn())?.abort();
await promptPromise;
await flushed;
const turn = await instance.getCurrentTurn();
Expand Down
1 change: 0 additions & 1 deletion packages/core/test/do/extension-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,6 @@ describe("createMockSession() — all methods reachable", () => {
await expect(s.sendUserMessage("hi")).resolves.toBeUndefined();
await expect(s.steer("steer")).resolves.toBeUndefined();
await expect(s.followUp("follow")).resolves.toBeUndefined();
await expect(s.abort()).resolves.toBeUndefined();
expect(await s.getModel()).toBe("test/model");
await expect(s.setModel("x")).resolves.toBeUndefined();
expect(await s.listModels()).toEqual([]);
Expand Down
1 change: 0 additions & 1 deletion packages/core/test/mocks/extension-stub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,6 @@ export function createMockSession(
sendUserMessage: async () => {},
steer: async () => {},
followUp: async () => {},
abort: async () => {},
getCurrentTurn: async () => undefined,
getModel: async () => "test/model",
setModel: async () => {},
Expand Down
6 changes: 3 additions & 3 deletions specs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,9 +311,6 @@ interface ISession extends IObservable<AgentEvent> {
// without interrupting an active turn.
followUp(text: string): Promise<void>;

// Abort the current streaming turn immediately.
abort(): Promise<void>;

// Return the active turn context (if a turn is in progress), or undefined if idle.
// Gateways call this after getHistory() to reconnect to an in-progress turn
// Returns undefined when idle; non-undefined means a turn is in progress.
Expand Down Expand Up @@ -410,6 +407,9 @@ interface ITurn {
// Tools call this to request interactive input mid-turn (select, confirm, input).
// Returns undefined if the gateway did not supply a callback for this turn.
getCallback(): Promise<IGatewayCallback | undefined>;

// Abort this turn if it is still running.
abort(): Promise<void>;
}
```

Expand Down
4 changes: 2 additions & 2 deletions specs/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ const session: ISession = await core.getSession(storedSessionId);

// All per-session operations are on the ISession stub
const turn = await session.prompt("Hello", attachments);
const stream = await turn.getStream();
await session.setModel("anthropic/claude-sonnet-4-5");
await session.abort();
const currentTurn = await session.getCurrentTurn();
await currentTurn?.abort();
```

See [api.md §1–2](api.md) for `IPiccoloCore` and `ISession`.
Expand Down
2 changes: 1 addition & 1 deletion specs/telegram_gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ Telegram `/commands` are handled by the gateway and not forwarded to the agent c
| `/new` | `core.newSession()`, update KV mapping, confirm to user |
| `/model <id>` | `session.setModel(id)`, confirm to user |
| `/models` | `core.listModels()`, reply with list |
| `/abort` | `session.abort()`, confirm to user |
| `/abort` | `session.getCurrentTurn()?.abort()`, confirm to user |
| `/status` | `session.getModel()` + `session.getContextUsage()`, reply with model + token usage |
| `/compact` | `session.compact()`, confirm to user |
| `/help` | Reply with list of available commands and descriptions |
Expand Down
Loading