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
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
id: 01M5J26DEM7Z0EHWJN3F79ZZZS
anchor: class Channel
created: 2026-10-22T15:03:10Z
norm: '1'
sig: c905677fa4be33c1
body_hash: 64e6dacda54276dc
raw_hash: 6d689f07f518e3a9
lines: 157-1008
---

an ask with `automatic` and no model must replace the chat's model with the channel's choice, so Retry can leave a model that fails
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function Composer
created: 2026-10-15T02:50:44Z
norm: '1'
sig: 6e0e6c5c8afbd01a
body_hash: f265dfc748a0c278
raw_hash: dd0efcf54d33e479
vouched: 2026-10-22T14:39:32Z
body_hash: 75fc838bf3451b29
raw_hash: 1948feaaac06338f
vouched: 2026-10-22T15:03:16Z
lines: 99-282
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function Composer
created: 2026-10-15T12:44:15Z
norm: '1'
sig: 6e0e6c5c8afbd01a
body_hash: f265dfc748a0c278
raw_hash: dd0efcf54d33e479
vouched: 2026-10-22T14:39:32Z
body_hash: 75fc838bf3451b29
raw_hash: 1948feaaac06338f
vouched: 2026-10-22T15:03:16Z
lines: 101-290
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function Composer
created: 2026-10-15T14:16:28Z
norm: '1'
sig: 6e0e6c5c8afbd01a
body_hash: f265dfc748a0c278
raw_hash: dd0efcf54d33e479
vouched: 2026-10-22T14:39:32Z
body_hash: 75fc838bf3451b29
raw_hash: 1948feaaac06338f
vouched: 2026-10-22T15:03:16Z
lines: 104-326
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: class Channel
created: 2026-10-14T17:33:22Z
norm: '1'
sig: c905677fa4be33c1
body_hash: e05cfd4b5bb4c4b3
raw_hash: aeb6b16210ed310a
vouched: 2026-10-22T14:35:52Z
body_hash: 64e6dacda54276dc
raw_hash: 6d689f07f518e3a9
vouched: 2026-10-22T15:03:17Z
lines: 114-792
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: class Channel
created: 2026-10-15T00:30:42Z
norm: '1'
sig: c905677fa4be33c1
body_hash: e05cfd4b5bb4c4b3
raw_hash: aeb6b16210ed310a
vouched: 2026-10-22T14:35:52Z
body_hash: 64e6dacda54276dc
raw_hash: 6d689f07f518e3a9
vouched: 2026-10-22T15:03:17Z
lines: 114-792
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: class Channel
created: 2026-10-15T02:34:37Z
norm: '1'
sig: c905677fa4be33c1
body_hash: e05cfd4b5bb4c4b3
raw_hash: aeb6b16210ed310a
vouched: 2026-10-22T14:35:52Z
body_hash: 64e6dacda54276dc
raw_hash: 6d689f07f518e3a9
vouched: 2026-10-22T15:03:17Z
lines: 114-794
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: class Channel
created: 2026-10-15T10:17:55Z
norm: '1'
sig: c905677fa4be33c1
body_hash: e05cfd4b5bb4c4b3
raw_hash: aeb6b16210ed310a
vouched: 2026-10-22T14:35:52Z
body_hash: 64e6dacda54276dc
raw_hash: 6d689f07f518e3a9
vouched: 2026-10-22T15:03:17Z
lines: 121-817
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: class Channel
created: 2026-10-16T22:11:35Z
norm: '1'
sig: c905677fa4be33c1
body_hash: e05cfd4b5bb4c4b3
raw_hash: aeb6b16210ed310a
vouched: 2026-10-22T14:35:52Z
body_hash: 64e6dacda54276dc
raw_hash: 6d689f07f518e3a9
vouched: 2026-10-22T15:03:17Z
lines: 127-873
---

Expand Down
61 changes: 61 additions & 0 deletions apps/app/src/composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ const LEADING = /^\s*@ace\b/i;
const UNPICKED: { model?: string; effort?: Effort } = {};

const key = (model: ModelRef) => `${model.provider}/${model.modelId}`;
// Errors a provider gives for the model or its key, which another try with them would repeat.
const MODEL_FAULT =
/\b(401|403|404)\b|api key|unauthori[sz]ed|authenticat|permission|does not exist|not found|access to/i;
const RETRY = "Try again.";

type Props = {
channel: Listing;
Expand All @@ -57,6 +61,8 @@ type Props = {
onDraftLoaded: () => void;
/** Messages withdrawn from the chat's queue; the user's own come back into the draft. */
returned: QueuedMessage[];
/** The error of the chat's latest run, when it failed and nothing has run since. */
failure?: string;
onProviders?: () => void;
accessory?: ReactNode;
};
Expand Down Expand Up @@ -89,6 +95,7 @@ export function Composer(
draft,
onDraftLoaded,
returned,
failure,
onProviders,
accessory,
}: Props,
Expand Down Expand Up @@ -265,6 +272,42 @@ export function Composer(
}
}

// Retrying a model's own failure on that model would fail again, so it hands the choice back.
const reset = !!failure && MODEL_FAULT.test(failure) && model !== "auto";
async function retry() {
if (sending.current) return;
sending.current = true;
setPending(true);
try {
const [provider, ...rest] = model.split("/");
await host.channel(channel.id, {
op: "ask",
chat: id,
author: user,
text: RETRY,
...(reset
? { automatic: true }
: {
...(levels ? { effort } : {}),
...(picked && picked !== "auto"
? { model: { provider: provider!, modelId: rest.join("/") } }
: {}),
}),
});
if (reset) {
changePick(UNPICKED);
toast("Switched this chat to Automatic", {
description: `${model} failed, so Retry let Ace choose the model.`,
});
}
} catch (error) {
toast.error("Could not retry", { description: (error as Error).message });
} finally {
sending.current = false;
setPending(false);
}
}

return (
<>
<ChatComposer
Expand Down Expand Up @@ -294,6 +337,24 @@ export function Composer(
clearOnSend={false}
onSend={({ doc, mode, attachments }) => void send(serialize(doc), mode, attachments)}
/>
{failure && !blocked && !busy && (
<Notice
action={
<Button
size="sm"
variant="outline"
disabled={!ready || status !== "open" || pending}
onClick={() => void retry()}
>
Retry
</Button>
}
>
{reset
? `The last run failed on ${model}. Retry switches this chat to Automatic.`
: "The last run failed."}
</Notice>
)}
{mode === "ace" && !offline && models?.length === 0 && (
<Notice
action={onProviders && (
Expand Down
3 changes: 3 additions & 0 deletions apps/app/src/conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ export function Conversation(
: "Sending";
const reply = transcript.items.findLast((item) => item.kind === "reply");
const question = reply?.kind === "reply" && pendingQuestion(reply.tools, transcript.busy);
const last = transcript.items.at(-1);
const failure = !transcript.busy && last?.kind === "reply" ? last.error : undefined;
const senders = useMemo(() => sources(channels, channel.host), [channels, channel.host]);
const events = useMemo(
() =>
Expand Down Expand Up @@ -299,6 +301,7 @@ export function Conversation(
draft={draft}
onDraftLoaded={onDraftLoaded}
returned={transcript.returned}
failure={failure}
onProviders={onProviders}
accessory={chat?.usage && <Usage value={chat.usage} context={chat.context} />}
/>
Expand Down
5 changes: 3 additions & 2 deletions packages/channel/src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,7 @@ export class Channel {
room.checkAttachments(request);
await this.#mayInvoke(request.author);
const conversation = await this.#conversation(request.chat);
await this.#select(conversation, request, request.model, request.effort);
await this.#select(conversation, request, request.model, request.effort, request.automatic);
const submission = await conversation.submit({
type: "input",
content: room.content(request.author, request.text, request),
Expand All @@ -678,11 +678,12 @@ export class Channel {
attached: room.Attached,
requested: ModelRef | undefined,
effort?: Effort,
automatic?: boolean,
) {
const agent = await conversation.agent(context);
const current = agent.model;
// A chat's inherited model can lose its provider key; only an explicit request must keep it.
const kept = !requested && current && await usable(this.#options.models, current)
const kept = !requested && !automatic && current && await usable(this.#options.models, current)
? current
: undefined;
const model = requested || kept || await (
Expand Down
2 changes: 2 additions & 0 deletions packages/channel/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ export type Request =
images?: Image[];
files?: TextFile[];
model?: ModelRef;
/** Without `model`, replace the chat's model with the channel's choice, as for a new chat. */
automatic?: boolean;
effort?: Effort;
requestId?: string;
}
Expand Down