Skip to content

Commit c7a59ee

Browse files
committed
better handling of aborting sessions
1 parent a272b58 commit c7a59ee

File tree

14 files changed

+368
-260
lines changed

14 files changed

+368
-260
lines changed

packages/opencode/src/session/index.ts

Lines changed: 168 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -552,167 +552,196 @@ export namespace Session {
552552
],
553553
}),
554554
})
555-
for await (const value of result.fullStream) {
556-
l.info("part", {
557-
type: value.type,
558-
})
559-
switch (value.type) {
560-
case "start":
561-
break
562-
563-
case "tool-input-start":
564-
next.parts.push({
565-
type: "tool",
566-
tool: value.toolName,
567-
id: value.id,
568-
state: {
569-
status: "pending",
570-
},
571-
})
572-
Bus.publish(MessageV2.Event.PartUpdated, {
573-
part: next.parts[next.parts.length - 1],
574-
sessionID: next.sessionID,
575-
messageID: next.id,
576-
})
577-
break
578-
579-
case "tool-input-delta":
580-
break
581-
582-
case "tool-call": {
583-
const match = next.parts.find((p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId)
584-
if (match) {
585-
match.state = {
586-
status: "running",
587-
input: value.input,
588-
time: {
589-
start: Date.now(),
555+
try {
556+
for await (const value of result.fullStream) {
557+
l.info("part", {
558+
type: value.type,
559+
})
560+
switch (value.type) {
561+
case "start":
562+
break
563+
564+
case "tool-input-start":
565+
next.parts.push({
566+
type: "tool",
567+
tool: value.toolName,
568+
id: value.id,
569+
state: {
570+
status: "pending",
590571
},
591-
}
572+
})
592573
Bus.publish(MessageV2.Event.PartUpdated, {
593-
part: match,
574+
part: next.parts[next.parts.length - 1],
594575
sessionID: next.sessionID,
595576
messageID: next.id,
596577
})
578+
break
579+
580+
case "tool-input-delta":
581+
break
582+
583+
case "tool-call": {
584+
const match = next.parts.find(
585+
(p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId,
586+
)
587+
if (match) {
588+
match.state = {
589+
status: "running",
590+
input: value.input,
591+
time: {
592+
start: Date.now(),
593+
},
594+
}
595+
Bus.publish(MessageV2.Event.PartUpdated, {
596+
part: match,
597+
sessionID: next.sessionID,
598+
messageID: next.id,
599+
})
600+
}
601+
break
597602
}
598-
break
599-
}
600-
case "tool-result": {
601-
const match = next.parts.find((p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId)
602-
if (match && match.state.status === "running") {
603-
match.state = {
604-
status: "completed",
605-
input: value.input,
606-
output: value.output.output,
607-
metadata: value.output.metadata,
608-
title: value.output.title,
609-
time: {
610-
start: match.state.time.start,
611-
end: Date.now(),
612-
},
603+
case "tool-result": {
604+
const match = next.parts.find(
605+
(p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId,
606+
)
607+
if (match && match.state.status === "running") {
608+
match.state = {
609+
status: "completed",
610+
input: value.input,
611+
output: value.output.output,
612+
metadata: value.output.metadata,
613+
title: value.output.title,
614+
time: {
615+
start: match.state.time.start,
616+
end: Date.now(),
617+
},
618+
}
619+
Bus.publish(MessageV2.Event.PartUpdated, {
620+
part: match,
621+
sessionID: next.sessionID,
622+
messageID: next.id,
623+
})
613624
}
614-
Bus.publish(MessageV2.Event.PartUpdated, {
615-
part: match,
616-
sessionID: next.sessionID,
617-
messageID: next.id,
618-
})
625+
break
619626
}
620-
break
621-
}
622627

623-
case "tool-error": {
624-
const match = next.parts.find((p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId)
625-
if (match && match.state.status === "running") {
626-
match.state = {
627-
status: "error",
628-
input: value.input,
629-
error: (value.error as any).toString(),
630-
time: {
631-
start: match.state.time.start,
632-
end: Date.now(),
633-
},
628+
case "tool-error": {
629+
const match = next.parts.find(
630+
(p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId,
631+
)
632+
if (match && match.state.status === "running") {
633+
match.state = {
634+
status: "error",
635+
input: value.input,
636+
error: (value.error as any).toString(),
637+
time: {
638+
start: match.state.time.start,
639+
end: Date.now(),
640+
},
641+
}
642+
Bus.publish(MessageV2.Event.PartUpdated, {
643+
part: match,
644+
sessionID: next.sessionID,
645+
messageID: next.id,
646+
})
634647
}
648+
break
649+
}
650+
651+
case "error":
652+
throw value.error
653+
654+
case "start-step":
655+
next.parts.push({
656+
type: "step-start",
657+
})
658+
break
659+
660+
case "finish-step":
661+
const usage = getUsage(model.info, value.usage, value.providerMetadata)
662+
next.cost += usage.cost
663+
next.tokens = usage.tokens
664+
break
665+
666+
case "text-start":
667+
text = {
668+
type: "text",
669+
text: "",
670+
}
671+
break
672+
673+
case "text":
674+
if (text.text === "") next.parts.push(text)
675+
text.text += value.text
676+
break
677+
678+
case "text-end":
635679
Bus.publish(MessageV2.Event.PartUpdated, {
636-
part: match,
680+
part: text,
637681
sessionID: next.sessionID,
638682
messageID: next.id,
639683
})
640-
}
641-
break
642-
}
684+
break
643685

644-
case "error":
645-
const e = value.error
646-
log.error("", {
647-
error: e,
648-
})
649-
switch (true) {
650-
case MessageV2.OutputLengthError.isInstance(e):
651-
next.error = e
652-
break
653-
case LoadAPIKeyError.isInstance(e):
654-
next.error = new Provider.AuthError(
655-
{
656-
providerID: input.providerID,
657-
message: e.message,
658-
},
659-
{ cause: e },
660-
).toObject()
661-
break
662-
case e instanceof Error:
663-
next.error = new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
664-
break
665-
default:
666-
next.error = new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e })
667-
}
668-
Bus.publish(Event.Error, {
669-
error: next.error,
670-
})
671-
break
686+
case "finish":
687+
next.time.completed = Date.now()
688+
break
672689

673-
case "start-step":
674-
next.parts.push({
675-
type: "step-start",
676-
})
677-
break
678-
679-
case "finish-step":
680-
const usage = getUsage(model.info, value.usage, value.providerMetadata)
681-
next.cost += usage.cost
682-
next.tokens = usage.tokens
683-
break
684-
685-
case "text-start":
686-
text = {
687-
type: "text",
688-
text: "",
689-
}
690+
default:
691+
l.info("unhandled", {
692+
...value,
693+
})
694+
continue
695+
}
696+
await updateMessage(next)
697+
}
698+
} catch (e) {
699+
log.error("", {
700+
error: e,
701+
})
702+
switch (true) {
703+
case e instanceof DOMException && e.name === "AbortError":
704+
next.error = new MessageV2.AbortedError(
705+
{ message: e.message },
706+
{
707+
cause: e,
708+
},
709+
).toObject()
690710
break
691-
692-
case "text":
693-
if (text.text === "") next.parts.push(text)
694-
text.text += value.text
711+
case MessageV2.OutputLengthError.isInstance(e):
712+
next.error = e
695713
break
696-
697-
case "text-end":
698-
Bus.publish(MessageV2.Event.PartUpdated, {
699-
part: text,
700-
sessionID: next.sessionID,
701-
messageID: next.id,
702-
})
714+
case LoadAPIKeyError.isInstance(e):
715+
next.error = new Provider.AuthError(
716+
{
717+
providerID: input.providerID,
718+
message: e.message,
719+
},
720+
{ cause: e },
721+
).toObject()
703722
break
704-
705-
case "finish":
706-
next.time.completed = Date.now()
723+
case e instanceof Error:
724+
next.error = new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
707725
break
708-
709726
default:
710-
l.info("unhandled", {
711-
...value,
712-
})
713-
continue
727+
next.error = new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e })
728+
}
729+
Bus.publish(Event.Error, {
730+
error: next.error,
731+
})
732+
}
733+
for (const part of next.parts) {
734+
if (part.type === "tool" && part.state.status !== "completed") {
735+
part.state = {
736+
status: "error",
737+
error: "Tool execution aborted",
738+
time: {
739+
start: Date.now(),
740+
end: Date.now(),
741+
},
742+
input: {},
743+
}
714744
}
715-
await updateMessage(next)
716745
}
717746
next.time.completed = Date.now()
718747
await updateMessage(next)

packages/opencode/src/session/message-v2.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { convertToModelMessages, type ModelMessage, type UIMessage } from "ai"
77

88
export namespace MessageV2 {
99
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
10+
export const AbortedError = NamedError.create("MessageAbortedError", z.object({}))
1011

1112
export const ToolStatePending = z
1213
.object({
@@ -148,7 +149,12 @@ export namespace MessageV2 {
148149
completed: z.number().optional(),
149150
}),
150151
error: z
151-
.discriminatedUnion("name", [Provider.AuthError.Schema, NamedError.Unknown.Schema, OutputLengthError.Schema])
152+
.discriminatedUnion("name", [
153+
Provider.AuthError.Schema,
154+
NamedError.Unknown.Schema,
155+
OutputLengthError.Schema,
156+
AbortedError.Schema,
157+
])
152158
.optional(),
153159
system: z.string().array(),
154160
modelID: z.string(),

packages/tui/internal/components/chat/messages.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,8 @@ func (m *messagesComponent) renderView(width int) {
320320
error = "Message output length exceeded"
321321
case opencode.ProviderAuthError:
322322
error = err.Data.Message
323+
case opencode.MessageAbortedError:
324+
error = "Request was aborted"
323325
case opencode.UnknownError:
324326
error = err.Data.Message
325327
}

packages/tui/sdk/.github/workflows/ci.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ name: CI
22
on:
33
push:
44
branches-ignore:
5-
- "generated"
6-
- "codegen/**"
7-
- "integrated/**"
8-
- "stl-preview-head/**"
9-
- "stl-preview-base/**"
5+
- 'generated'
6+
- 'codegen/**'
7+
- 'integrated/**'
8+
- 'stl-preview-head/**'
9+
- 'stl-preview-base/**'
1010
pull_request:
1111
branches-ignore:
12-
- "stl-preview-head/**"
13-
- "stl-preview-base/**"
12+
- 'stl-preview-head/**'
13+
- 'stl-preview-base/**'
1414

1515
jobs:
1616
lint:
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
22
".": "0.1.0-alpha.8"
3-
}
3+
}

0 commit comments

Comments
 (0)