mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-08 17:59:09 +00:00
Refactor agent loop (#4412)
This commit is contained in:
@@ -142,6 +142,21 @@ export namespace MessageV2 {
|
||||
})
|
||||
export type AgentPart = z.infer<typeof AgentPart>
|
||||
|
||||
export const CompactionPart = PartBase.extend({
|
||||
type: z.literal("compaction"),
|
||||
}).meta({
|
||||
ref: "CompactionPart",
|
||||
})
|
||||
export type CompactionPart = z.infer<typeof CompactionPart>
|
||||
|
||||
export const SubtaskPart = PartBase.extend({
|
||||
type: z.literal("subtask"),
|
||||
prompt: z.string(),
|
||||
description: z.string(),
|
||||
agent: z.string(),
|
||||
})
|
||||
export type SubtaskPart = z.infer<typeof SubtaskPart>
|
||||
|
||||
export const RetryPart = PartBase.extend({
|
||||
type: z.literal("retry"),
|
||||
attempt: z.number(),
|
||||
@@ -277,6 +292,13 @@ export namespace MessageV2 {
|
||||
diffs: Snapshot.FileDiff.array(),
|
||||
})
|
||||
.optional(),
|
||||
agent: z.string(),
|
||||
model: z.object({
|
||||
providerID: z.string(),
|
||||
modelID: z.string(),
|
||||
}),
|
||||
system: z.string().optional(),
|
||||
tools: z.record(z.string(), z.boolean()).optional(),
|
||||
}).meta({
|
||||
ref: "UserMessage",
|
||||
})
|
||||
@@ -285,6 +307,7 @@ export namespace MessageV2 {
|
||||
export const Part = z
|
||||
.discriminatedUnion("type", [
|
||||
TextPart,
|
||||
SubtaskPart,
|
||||
ReasoningPart,
|
||||
FilePart,
|
||||
ToolPart,
|
||||
@@ -294,6 +317,7 @@ export namespace MessageV2 {
|
||||
PatchPart,
|
||||
AgentPart,
|
||||
RetryPart,
|
||||
CompactionPart,
|
||||
])
|
||||
.meta({
|
||||
ref: "Part",
|
||||
@@ -334,6 +358,7 @@ export namespace MessageV2 {
|
||||
write: z.number(),
|
||||
}),
|
||||
}),
|
||||
finish: z.string().optional(),
|
||||
}).meta({
|
||||
ref: "AssistantMessage",
|
||||
})
|
||||
@@ -482,6 +507,11 @@ export namespace MessageV2 {
|
||||
time: {
|
||||
created: v1.metadata.time.created,
|
||||
},
|
||||
agent: "build",
|
||||
model: {
|
||||
providerID: "opencode",
|
||||
modelID: "opencode",
|
||||
},
|
||||
}
|
||||
const parts = v1.parts.flatMap((part): Part[] => {
|
||||
const base = {
|
||||
@@ -529,107 +559,107 @@ export namespace MessageV2 {
|
||||
if (msg.parts.length === 0) continue
|
||||
|
||||
if (msg.info.role === "user") {
|
||||
result.push({
|
||||
const userMessage: UIMessage = {
|
||||
id: msg.info.id,
|
||||
role: "user",
|
||||
parts: msg.parts.flatMap((part): UIMessage["parts"] => {
|
||||
if (part.type === "text")
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text: part.text,
|
||||
},
|
||||
]
|
||||
// text/plain and directory files are converted into text parts, ignore them
|
||||
if (part.type === "file" && part.mime !== "text/plain" && part.mime !== "application/x-directory")
|
||||
return [
|
||||
{
|
||||
type: "file",
|
||||
url: part.url,
|
||||
mediaType: part.mime,
|
||||
filename: part.filename,
|
||||
},
|
||||
]
|
||||
return []
|
||||
}),
|
||||
})
|
||||
parts: [],
|
||||
}
|
||||
result.push(userMessage)
|
||||
for (const part of msg.parts) {
|
||||
if (part.type === "text")
|
||||
userMessage.parts.push({
|
||||
type: "text",
|
||||
text: part.text,
|
||||
})
|
||||
// text/plain and directory files are converted into text parts, ignore them
|
||||
if (part.type === "file" && part.mime !== "text/plain" && part.mime !== "application/x-directory")
|
||||
userMessage.parts.push({
|
||||
type: "file",
|
||||
url: part.url,
|
||||
mediaType: part.mime,
|
||||
filename: part.filename,
|
||||
})
|
||||
|
||||
if (part.type === "compaction") {
|
||||
userMessage.parts.push({
|
||||
type: "text",
|
||||
text: "What did we do so far?",
|
||||
})
|
||||
}
|
||||
if (part.type === "subtask") {
|
||||
userMessage.parts.push({
|
||||
type: "text",
|
||||
text: "The following tool was executed by the user",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.info.role === "assistant") {
|
||||
result.push({
|
||||
const assistantMessage: UIMessage = {
|
||||
id: msg.info.id,
|
||||
role: "assistant",
|
||||
parts: msg.parts.flatMap((part): UIMessage["parts"] => {
|
||||
if (part.type === "text")
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text: part.text,
|
||||
providerMetadata: part.metadata,
|
||||
},
|
||||
]
|
||||
if (part.type === "step-start")
|
||||
return [
|
||||
{
|
||||
type: "step-start",
|
||||
},
|
||||
]
|
||||
if (part.type === "tool") {
|
||||
if (part.state.status === "completed") {
|
||||
if (part.state.attachments?.length) {
|
||||
result.push({
|
||||
id: Identifier.ascending("message"),
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Tool ${part.tool} returned an attachment:`,
|
||||
},
|
||||
...part.state.attachments.map((attachment) => ({
|
||||
type: "file" as const,
|
||||
url: attachment.url,
|
||||
mediaType: attachment.mime,
|
||||
filename: attachment.filename,
|
||||
})),
|
||||
],
|
||||
})
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: ("tool-" + part.tool) as `tool-${string}`,
|
||||
state: "output-available",
|
||||
toolCallId: part.callID,
|
||||
input: part.state.input,
|
||||
output: part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output,
|
||||
callProviderMetadata: part.metadata,
|
||||
},
|
||||
]
|
||||
parts: [],
|
||||
}
|
||||
result.push(assistantMessage)
|
||||
for (const part of msg.parts) {
|
||||
if (part.type === "text")
|
||||
assistantMessage.parts.push({
|
||||
type: "text",
|
||||
text: part.text,
|
||||
providerMetadata: part.metadata,
|
||||
})
|
||||
if (part.type === "step-start")
|
||||
assistantMessage.parts.push({
|
||||
type: "step-start",
|
||||
})
|
||||
if (part.type === "tool") {
|
||||
if (part.state.status === "completed") {
|
||||
if (part.state.attachments?.length) {
|
||||
result.push({
|
||||
id: Identifier.ascending("message"),
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Tool ${part.tool} returned an attachment:`,
|
||||
},
|
||||
...part.state.attachments.map((attachment) => ({
|
||||
type: "file" as const,
|
||||
url: attachment.url,
|
||||
mediaType: attachment.mime,
|
||||
filename: attachment.filename,
|
||||
})),
|
||||
],
|
||||
})
|
||||
}
|
||||
if (part.state.status === "error")
|
||||
return [
|
||||
{
|
||||
type: ("tool-" + part.tool) as `tool-${string}`,
|
||||
state: "output-error",
|
||||
toolCallId: part.callID,
|
||||
input: part.state.input,
|
||||
errorText: part.state.error,
|
||||
callProviderMetadata: part.metadata,
|
||||
},
|
||||
]
|
||||
assistantMessage.parts.push({
|
||||
type: ("tool-" + part.tool) as `tool-${string}`,
|
||||
state: "output-available",
|
||||
toolCallId: part.callID,
|
||||
input: part.state.input,
|
||||
output: part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output,
|
||||
callProviderMetadata: part.metadata,
|
||||
})
|
||||
}
|
||||
if (part.type === "reasoning") {
|
||||
return [
|
||||
{
|
||||
type: "reasoning",
|
||||
text: part.text,
|
||||
providerMetadata: part.metadata,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return []
|
||||
}),
|
||||
})
|
||||
if (part.state.status === "error")
|
||||
assistantMessage.parts.push({
|
||||
type: ("tool-" + part.tool) as `tool-${string}`,
|
||||
state: "output-error",
|
||||
toolCallId: part.callID,
|
||||
input: part.state.input,
|
||||
errorText: part.state.error,
|
||||
callProviderMetadata: part.metadata,
|
||||
})
|
||||
}
|
||||
if (part.type === "reasoning") {
|
||||
assistantMessage.parts.push({
|
||||
type: "reasoning",
|
||||
text: part.text,
|
||||
providerMetadata: part.metadata,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,9 +701,16 @@ export namespace MessageV2 {
|
||||
|
||||
export async function filterCompacted(stream: AsyncIterable<MessageV2.WithParts>) {
|
||||
const result = [] as MessageV2.WithParts[]
|
||||
const completed = new Set<string>()
|
||||
for await (const msg of stream) {
|
||||
result.push(msg)
|
||||
if (msg.info.role === "assistant" && msg.info.summary === true) break
|
||||
if (
|
||||
msg.info.role === "user" &&
|
||||
completed.has(msg.info.id) &&
|
||||
msg.parts.some((part) => part.type === "compaction")
|
||||
)
|
||||
break
|
||||
if (msg.info.role === "assistant" && msg.info.summary && msg.info.finish) completed.add(msg.info.parentID)
|
||||
}
|
||||
result.reverse()
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user