Compare commits
5 Commits
dev
...
fix/stale-
| Author | SHA1 | Date |
|---|---|---|
|
|
2ef6c88a0b | |
|
|
c19cbccf06 | |
|
|
5a7a86810a | |
|
|
6bfce604bf | |
|
|
060f482eb2 |
|
|
@ -151,7 +151,7 @@ export async function bootstrapDirectory(input: {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
input.sdk.path.get().then((x) => input.setStore("path", x.data!)),
|
input.sdk.path.get().then((x) => input.setStore("path", x.data!)),
|
||||||
input.sdk.command.list().then((x) => input.setStore("command", x.data ?? [])),
|
input.sdk.command.list().then((x) => input.setStore("command", x.data ?? [])),
|
||||||
input.sdk.session.status().then((x) => input.setStore("session_status", x.data!)),
|
input.sdk.session.status().then((x) => input.setStore("session_status", reconcile(x.data!))),
|
||||||
input.loadSessions(input.directory),
|
input.loadSessions(input.directory),
|
||||||
input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!)),
|
input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!)),
|
||||||
input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!)),
|
input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!)),
|
||||||
|
|
|
||||||
|
|
@ -204,18 +204,8 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||||
})
|
})
|
||||||
const isWorking = createMemo(() => {
|
const isWorking = createMemo(() => {
|
||||||
if (hasPermissions()) return false
|
if (hasPermissions()) return false
|
||||||
const pending = (sessionStore.message[props.session.id] ?? []).findLast(
|
|
||||||
(message) =>
|
|
||||||
message.role === "assistant" &&
|
|
||||||
typeof (message as { time?: { completed?: unknown } }).time?.completed !== "number",
|
|
||||||
)
|
|
||||||
const status = sessionStore.session_status[props.session.id]
|
const status = sessionStore.session_status[props.session.id]
|
||||||
return (
|
return status !== undefined && status.type !== "idle"
|
||||||
pending !== undefined ||
|
|
||||||
status?.type === "busy" ||
|
|
||||||
status?.type === "retry" ||
|
|
||||||
(status !== undefined && status.type !== "idle")
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const tint = createMemo(() => {
|
const tint = createMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -1361,10 +1361,8 @@ export default function Page() {
|
||||||
})
|
})
|
||||||
|
|
||||||
const busy = (sessionID: string) => {
|
const busy = (sessionID: string) => {
|
||||||
if ((sync.data.session_status[sessionID] ?? { type: "idle" as const }).type !== "idle") return true
|
const status = sync.data.session_status[sessionID]
|
||||||
return (sync.data.message[sessionID] ?? []).some(
|
return status !== undefined && status.type !== "idle"
|
||||||
(item) => item.role === "assistant" && typeof item.time.completed !== "number",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const queuedFollowups = createMemo(() => {
|
const queuedFollowups = createMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -236,17 +236,17 @@ export function MessageTimeline(props: {
|
||||||
if (!id) return emptyMessages
|
if (!id) return emptyMessages
|
||||||
return sync.data.message[id] ?? emptyMessages
|
return sync.data.message[id] ?? emptyMessages
|
||||||
})
|
})
|
||||||
const pending = createMemo(() =>
|
const assistant = createMemo(() => {
|
||||||
sessionMessages().findLast(
|
const item = sessionMessages().findLast((item): item is AssistantMessage => item.role === "assistant")
|
||||||
(item): item is AssistantMessage => item.role === "assistant" && typeof item.time.completed !== "number",
|
if (!item || typeof item.time.completed === "number") return
|
||||||
),
|
return item
|
||||||
)
|
})
|
||||||
const sessionStatus = createMemo(() => {
|
const sessionStatus = createMemo(() => {
|
||||||
const id = sessionID()
|
const id = sessionID()
|
||||||
if (!id) return idle
|
if (!id) return idle
|
||||||
return sync.data.session_status[id] ?? idle
|
return sync.data.session_status[id] ?? idle
|
||||||
})
|
})
|
||||||
const working = createMemo(() => !!pending() || sessionStatus().type !== "idle")
|
const busy = createMemo(() => sessionStatus().type !== "idle")
|
||||||
const tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent))
|
const tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent))
|
||||||
|
|
||||||
const [slot, setSlot] = createStore({
|
const [slot, setSlot] = createStore({
|
||||||
|
|
@ -264,7 +264,7 @@ export function MessageTimeline(props: {
|
||||||
onCleanup(clear)
|
onCleanup(clear)
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
working,
|
busy,
|
||||||
(on, prev) => {
|
(on, prev) => {
|
||||||
clear()
|
clear()
|
||||||
if (on) {
|
if (on) {
|
||||||
|
|
@ -282,7 +282,9 @@ export function MessageTimeline(props: {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const activeMessageID = createMemo(() => {
|
const activeMessageID = createMemo(() => {
|
||||||
const parentID = pending()?.parentID
|
if (!busy()) return undefined
|
||||||
|
|
||||||
|
const parentID = assistant()?.parentID
|
||||||
if (parentID) {
|
if (parentID) {
|
||||||
const messages = sessionMessages()
|
const messages = sessionMessages()
|
||||||
const result = Binary.search(messages, parentID, (message) => message.id)
|
const result = Binary.search(messages, parentID, (message) => message.id)
|
||||||
|
|
@ -290,15 +292,10 @@ export function MessageTimeline(props: {
|
||||||
if (message && message.role === "user") return message.id
|
if (message && message.role === "user") return message.id
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = sessionStatus()
|
const messages = sessionMessages()
|
||||||
if (status.type !== "idle") {
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
const messages = sessionMessages()
|
if (messages[i].role === "user") return messages[i].id
|
||||||
for (let i = messages.length - 1; i >= 0; i--) {
|
|
||||||
if (messages[i].role === "user") return messages[i].id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined
|
|
||||||
})
|
})
|
||||||
const info = createMemo(() => {
|
const info = createMemo(() => {
|
||||||
const id = sessionID()
|
const id = sessionID()
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,9 @@ export function Session() {
|
||||||
})
|
})
|
||||||
|
|
||||||
const pending = createMemo(() => {
|
const pending = createMemo(() => {
|
||||||
return messages().findLast((x) => x.role === "assistant" && !x.time.completed)?.id
|
const last = messages().findLast((x) => x.role === "assistant")
|
||||||
|
if (!last || last.time.completed) return
|
||||||
|
return last.id
|
||||||
})
|
})
|
||||||
|
|
||||||
const lastAssistant = createMemo(() => {
|
const lastAssistant = createMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { MessageV2 } from "./message-v2"
|
||||||
|
|
||||||
|
export const done = (msg: MessageV2.Assistant, time = Date.now()) => {
|
||||||
|
if (typeof msg.time.completed === "number") return msg
|
||||||
|
msg.time.completed = time
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ import { Config } from "@/config/config"
|
||||||
import { SessionCompaction } from "./compaction"
|
import { SessionCompaction } from "./compaction"
|
||||||
import { PermissionNext } from "@/permission"
|
import { PermissionNext } from "@/permission"
|
||||||
import { Question } from "@/question"
|
import { Question } from "@/question"
|
||||||
|
import { done } from "./assistant"
|
||||||
import { PartID } from "./schema"
|
import { PartID } from "./schema"
|
||||||
import type { SessionID, MessageID } from "./schema"
|
import type { SessionID, MessageID } from "./schema"
|
||||||
|
|
||||||
|
|
@ -416,7 +417,7 @@ export namespace SessionProcessor {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input.assistantMessage.time.completed = Date.now()
|
done(input.assistantMessage)
|
||||||
await Session.updateMessage(input.assistantMessage)
|
await Session.updateMessage(input.assistantMessage)
|
||||||
if (needsCompaction) return "compact"
|
if (needsCompaction) return "compact"
|
||||||
if (blocked) return "stop"
|
if (blocked) return "stop"
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import fs from "fs/promises"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util/filesystem"
|
||||||
import { SessionID, MessageID, PartID } from "./schema"
|
import { SessionID, MessageID, PartID } from "./schema"
|
||||||
|
import { done } from "./assistant"
|
||||||
import { MessageV2 } from "./message-v2"
|
import { MessageV2 } from "./message-v2"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { SessionRevert } from "./revert"
|
import { SessionRevert } from "./revert"
|
||||||
|
|
@ -465,7 +466,7 @@ export namespace SessionPrompt {
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assistantMessage.finish = "tool-calls"
|
assistantMessage.finish = "tool-calls"
|
||||||
assistantMessage.time.completed = Date.now()
|
done(assistantMessage)
|
||||||
await Session.updateMessage(assistantMessage)
|
await Session.updateMessage(assistantMessage)
|
||||||
if (result && part.state.status === "running") {
|
if (result && part.state.status === "running") {
|
||||||
await Session.updatePart({
|
await Session.updatePart({
|
||||||
|
|
@ -599,91 +600,100 @@ export namespace SessionPrompt {
|
||||||
})
|
})
|
||||||
using _ = defer(() => InstructionPrompt.clear(processor.message.id))
|
using _ = defer(() => InstructionPrompt.clear(processor.message.id))
|
||||||
|
|
||||||
// Check if user explicitly invoked an agent via @ in this turn
|
const format = lastUser.format ?? { type: "text" }
|
||||||
const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
|
const result = await (async () => {
|
||||||
const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? false
|
const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
|
||||||
|
const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? false
|
||||||
|
|
||||||
const tools = await resolveTools({
|
const tools = await resolveTools({
|
||||||
agent,
|
agent,
|
||||||
session,
|
session,
|
||||||
model,
|
model,
|
||||||
tools: lastUser.tools,
|
tools: lastUser.tools,
|
||||||
processor,
|
processor,
|
||||||
bypassAgentCheck,
|
bypassAgentCheck,
|
||||||
messages: msgs,
|
messages: msgs,
|
||||||
})
|
|
||||||
|
|
||||||
// Inject StructuredOutput tool if JSON schema mode enabled
|
|
||||||
if (lastUser.format?.type === "json_schema") {
|
|
||||||
tools["StructuredOutput"] = createStructuredOutputTool({
|
|
||||||
schema: lastUser.format.schema,
|
|
||||||
onSuccess(output) {
|
|
||||||
structuredOutput = output
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
if (step === 1) {
|
if (format.type === "json_schema") {
|
||||||
SessionSummary.summarize({
|
tools["StructuredOutput"] = createStructuredOutputTool({
|
||||||
sessionID: sessionID,
|
schema: format.schema,
|
||||||
messageID: lastUser.id,
|
onSuccess(output) {
|
||||||
})
|
structuredOutput = output
|
||||||
}
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Ephemerally wrap queued user messages with a reminder to stay on track
|
if (step === 1) {
|
||||||
if (step > 1 && lastFinished) {
|
SessionSummary.summarize({
|
||||||
for (const msg of msgs) {
|
sessionID: sessionID,
|
||||||
if (msg.info.role !== "user" || msg.info.id <= lastFinished.id) continue
|
messageID: lastUser.id,
|
||||||
for (const part of msg.parts) {
|
})
|
||||||
if (part.type !== "text" || part.ignored || part.synthetic) continue
|
}
|
||||||
if (!part.text.trim()) continue
|
|
||||||
part.text = [
|
if (step > 1 && lastFinished) {
|
||||||
"<system-reminder>",
|
for (const msg of msgs) {
|
||||||
"The user sent the following message:",
|
if (msg.info.role !== "user" || msg.info.id <= lastFinished.id) continue
|
||||||
part.text,
|
for (const part of msg.parts) {
|
||||||
"",
|
if (part.type !== "text" || part.ignored || part.synthetic) continue
|
||||||
"Please address this message and continue with your tasks.",
|
if (!part.text.trim()) continue
|
||||||
"</system-reminder>",
|
part.text = [
|
||||||
].join("\n")
|
"<system-reminder>",
|
||||||
|
"The user sent the following message:",
|
||||||
|
part.text,
|
||||||
|
"",
|
||||||
|
"Please address this message and continue with your tasks.",
|
||||||
|
"</system-reminder>",
|
||||||
|
].join("\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
await Plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
|
await Plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
|
||||||
|
|
||||||
// Build system prompt, adding structured output instruction if needed
|
const skills = await SystemPrompt.skills(agent)
|
||||||
const skills = await SystemPrompt.skills(agent)
|
const system = [
|
||||||
const system = [
|
...(await SystemPrompt.environment(model)),
|
||||||
...(await SystemPrompt.environment(model)),
|
...(skills ? [skills] : []),
|
||||||
...(skills ? [skills] : []),
|
...(await InstructionPrompt.system()),
|
||||||
...(await InstructionPrompt.system()),
|
]
|
||||||
]
|
if (format.type === "json_schema") {
|
||||||
const format = lastUser.format ?? { type: "text" }
|
system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT)
|
||||||
if (format.type === "json_schema") {
|
}
|
||||||
system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT)
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await processor.process({
|
return processor.process({
|
||||||
user: lastUser,
|
user: lastUser,
|
||||||
agent,
|
agent,
|
||||||
permission: session.permission,
|
permission: session.permission,
|
||||||
abort,
|
abort,
|
||||||
sessionID,
|
sessionID,
|
||||||
system,
|
system,
|
||||||
messages: [
|
messages: [
|
||||||
...MessageV2.toModelMessages(msgs, model),
|
...MessageV2.toModelMessages(msgs, model),
|
||||||
...(isLastStep
|
...(isLastStep
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
role: "assistant" as const,
|
role: "assistant" as const,
|
||||||
content: MAX_STEPS,
|
content: MAX_STEPS,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
],
|
],
|
||||||
tools,
|
tools,
|
||||||
model,
|
model,
|
||||||
toolChoice: format.type === "json_schema" ? "required" : undefined,
|
toolChoice: format.type === "json_schema" ? "required" : undefined,
|
||||||
|
})
|
||||||
|
})().catch(async (err) => {
|
||||||
|
if (typeof processor.message.time.completed !== "number") {
|
||||||
|
await Session.updateMessage(done(processor.message)).catch((cause) => {
|
||||||
|
log.error("failed to finalize assistant after prompt error", {
|
||||||
|
cause,
|
||||||
|
messageID: processor.message.id,
|
||||||
|
sessionID,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
throw err
|
||||||
})
|
})
|
||||||
|
|
||||||
// If structured output was captured, save it and exit immediately
|
// If structured output was captured, save it and exit immediately
|
||||||
|
|
@ -1723,7 +1733,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||||
if (aborted) {
|
if (aborted) {
|
||||||
output += "\n\n" + ["<metadata>", "User aborted the command", "</metadata>"].join("\n")
|
output += "\n\n" + ["<metadata>", "User aborted the command", "</metadata>"].join("\n")
|
||||||
}
|
}
|
||||||
msg.time.completed = Date.now()
|
done(msg)
|
||||||
await Session.updateMessage(msg)
|
await Session.updateMessage(msg)
|
||||||
if (part.state.status === "running") {
|
if (part.state.status === "running") {
|
||||||
part.state = {
|
part.state = {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||||
|
import { done } from "../../src/session/assistant"
|
||||||
|
import type { MessageV2 } from "../../src/session/message-v2"
|
||||||
|
import { SessionID } from "../../src/session/schema"
|
||||||
|
|
||||||
|
const sessionID = SessionID.make("session")
|
||||||
|
const providerID = ProviderID.make("test")
|
||||||
|
const modelID = ModelID.make("model")
|
||||||
|
|
||||||
|
const assistant = (completed?: number) =>
|
||||||
|
({
|
||||||
|
id: "msg_1",
|
||||||
|
sessionID,
|
||||||
|
parentID: "msg_0",
|
||||||
|
role: "assistant",
|
||||||
|
time: completed === undefined ? { created: 1 } : { created: 1, completed },
|
||||||
|
mode: "build",
|
||||||
|
agent: "build",
|
||||||
|
path: { cwd: "/", root: "/" },
|
||||||
|
cost: 0,
|
||||||
|
tokens: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
reasoning: 0,
|
||||||
|
cache: { read: 0, write: 0 },
|
||||||
|
},
|
||||||
|
modelID,
|
||||||
|
providerID,
|
||||||
|
}) as MessageV2.Assistant
|
||||||
|
|
||||||
|
describe("session assistant", () => {
|
||||||
|
test("marks incomplete assistants as done", () => {
|
||||||
|
const msg = assistant()
|
||||||
|
|
||||||
|
expect(done(msg, 10).time.completed).toBe(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("preserves existing completion time", () => {
|
||||||
|
const msg = assistant(5)
|
||||||
|
|
||||||
|
expect(done(msg, 10).time.completed).toBe(5)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -195,9 +195,9 @@ export function SessionTurn(
|
||||||
const pending = createMemo(() => {
|
const pending = createMemo(() => {
|
||||||
if (typeof props.active === "boolean") return
|
if (typeof props.active === "boolean") return
|
||||||
const messages = allMessages() ?? emptyMessages
|
const messages = allMessages() ?? emptyMessages
|
||||||
return messages.findLast(
|
const item = messages.findLast((item): item is AssistantMessage => item.role === "assistant")
|
||||||
(item): item is AssistantMessage => item.role === "assistant" && typeof item.time.completed !== "number",
|
if (!item || typeof item.time.completed === "number") return
|
||||||
)
|
return item
|
||||||
})
|
})
|
||||||
|
|
||||||
const pendingUser = createMemo(() => {
|
const pendingUser = createMemo(() => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue