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
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions packages/core/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1271,6 +1271,11 @@ export const messages = {
},
"tool.error.skill": { "zh-CN": "无法加载技能 {{name}}", en: "Unable to load skill {{name}}" },
"tool.error.task_cancelled": { "zh-CN": "任务已取消", en: "Task cancelled" },
"tool.error.subagent_interrupted": {
"zh-CN":
"子代理任务被中断。要继续此任务,请使用相同的 subagent_type 并设置 task_id 为 {{sessionId}} 重新调用 task 工具,子代理将从上次中断处继续执行{{detail}}",
en: "The subagent task was interrupted. To resume, call the task tool again with the same subagent_type and task_id set to {{sessionId}}; the subagent will continue from where it was interrupted{{detail}}",
},
"tool.error.unknown_agent": {
"zh-CN": "未知代理类型:{{type}} 不是有效的代理类型",
en: "Unknown agent type: {{type}} is not a valid agent type",
Expand Down Expand Up @@ -2472,8 +2477,9 @@ export const messages = {
en: 'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.',
},
"tool.param.task_resume": {
"zh-CN": "仅在要恢复之前的任务时设置;传入之前的 task_id 可继续同一代理会话,而不是创建新会话",
en: "Set this only when resuming a previous task; pass a prior task_id to continue the same subagent session instead of creating a fresh one",
"zh-CN":
"仅在要恢复之前的任务时设置;传入之前的 task_id 可继续同一代理会话,而不是创建新会话。如果子代理被中断,传入此参数可从上次中断处继续执行",
en: "Set this only when resuming a previous task; pass a prior task_id to continue the same subagent session instead of creating a fresh one. If the subagent was interrupted, passing this will resume from where it left off",
},
"tool.param.task_background": {
"zh-CN": "在后台运行代理;完成后会收到通知。不要休眠、轮询或主动检查进度",
Expand Down
28 changes: 23 additions & 5 deletions packages/miaopan-code/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ const layer = Layer.effect(
cancel: (sessionID: SessionID) => cancel(sessionID),
resolvePromptParts: (template: string) => resolvePromptParts(template),
prompt: (input: PromptInput) => prompt(input).pipe(Effect.catch(Effect.die)),
continue: (input: LoopInput) => continueSession(input).pipe(Effect.catch(Effect.die)),
} satisfies TaskPromptOps
})

Expand Down Expand Up @@ -371,13 +372,19 @@ const layer = Layer.effect(
assistantMessage.time.completed = Date.now()
yield* sessions.updateMessage(assistantMessage)
if (part.state.status === "running") {
const cfg = yield* config.get()
const sessionId = part.state.metadata?.sessionId as string | undefined
yield* sessions.updatePart({
...part,
state: {
status: "error",
error: t((yield* config.get()).language, "error.tool_cancelled"),
error: t(cfg.language, "error.tool_cancelled"),
time: { start: part.state.time.start, end: Date.now() },
metadata: part.state.metadata,
metadata: {
...(part.state.metadata ?? {}),
interrupted: true,
...(sessionId ? { sessionId } : {}),
},
input: part.state.input,
},
} satisfies SessionV1.ToolPart)
Expand Down Expand Up @@ -419,13 +426,24 @@ const layer = Layer.effect(
}

if (!result) {
const stateMetadata = part.state.status === "pending" ? undefined : part.state.metadata
const wasInterrupted = stateMetadata?.interrupted === true
const sessionId = stateMetadata?.sessionId as string | undefined
const cfg = yield* config.get()
yield* sessions.updatePart({
...part,
state: {
status: "error",
error: t((yield* config.get()).language, "error.tool_execution_failed", {
detail: error ? `: ${error.message}` : "",
}),
error: wasInterrupted
? sessionId
? t(cfg.language, "tool.error.subagent_interrupted", {
detail: error ? `: ${error.message}` : "",
sessionId,
})
: t(cfg.language, "error.tool_cancelled")
: t(cfg.language, "error.tool_execution_failed", {
detail: error ? `: ${error.message}` : "",
}),
time: {
start: part.state.status === "running" ? part.state.time.start : Date.now(),
end: Date.now(),
Expand Down
20 changes: 19 additions & 1 deletion packages/miaopan-code/src/tool/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Agent } from "../agent/agent"
import { deriveSubagentSessionPermission } from "../agent/subagent-permissions"
import type { SessionPrompt } from "../session/prompt"
import { Config } from "@/config/config"
import { Effect, Exit, Schema, Scope } from "effect"
import { Effect, Exit, Option, Schema, Scope } from "effect"
import { EffectBridge } from "@/effect/bridge"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { Database } from "@miaopan-code/core/database/database"
Expand All @@ -25,6 +25,7 @@ export interface TaskPromptOps {
cancel(sessionID: SessionID): Effect.Effect<void>
resolvePromptParts(template: string): Effect.Effect<SessionPrompt.PromptInput["parts"]>
prompt(input: SessionPrompt.PromptInput): Effect.Effect<SessionV1.WithParts>
continue(input: SessionPrompt.LoopInput): Effect.Effect<SessionV1.WithParts>
}

const id = "task"
Expand Down Expand Up @@ -227,6 +228,23 @@ export const TaskTool = Tool.define(
const oai = parentMessage.info.role === "user" ? parentMessage.info.oai : undefined

const runTask = Effect.fn("TaskTool.runTask")(function* () {
if (session) {
const lastAssistant = yield* sessions
.findMessage(nextSession.id, (message) => message.info.role === "assistant")
.pipe(Effect.orDie)
const wasInterrupted =
Option.isSome(lastAssistant) &&
lastAssistant.value.info.role === "assistant" &&
lastAssistant.value.info.error?.name === "MessageAbortedError"
if (wasInterrupted) {
const result = yield* ops.continue({
sessionID: nextSession.id,
})
const text = result.parts.findLast((item) => item.type === "text")?.text ?? ""
if (!builtinReview) return text
return Review.renderOutput(Review.parseOutput(text), language)
}
}
const parts = yield* ops.resolvePromptParts(params.prompt)
const result = yield* ops.prompt({
messageID: MessageID.ascending(),
Expand Down
Loading