Files
curriculum-project-hub/hub/src/feishu/runOutcomeNotice.ts
T
hongjr03 6cefb2a938 feat(hub): raise agent turns/time limits and notify teachers on failure
Defaults and silo env go to 150 turns / 1800s wall clock. Run completion
appends a clear Feishu notice for max-turns, timeout, and other failures
(partial answer kept). Startup process-restart kills notify the bound chat.
Release v0.0.38.
2026-07-20 12:07:45 +00:00

107 lines
3.6 KiB
TypeScript

/**
* User-visible run termination copy for Feishu teachers.
* Keep messages short, actionable, and free of stack traces.
*/
export interface RunOutcomeNoticeInput {
readonly wallTimeExceeded: boolean;
readonly interrupted: boolean;
readonly resultStatus: string;
readonly resultError: string | undefined;
readonly maxTurns: number;
readonly maxRunSeconds: number;
readonly hasPartialText: boolean;
}
export interface RunOutcomeNotice {
/** Mark the streaming card as failed (red footer). */
readonly isError: boolean;
/**
* Teacher-facing explanation. Appended after any partial answer text so the
* cause of a stop is never silent.
*/
readonly notice: string | undefined;
}
export function teacherFacingRunOutcome(input: RunOutcomeNoticeInput): RunOutcomeNotice {
if (input.wallTimeExceeded) {
return {
isError: true,
notice: noticeLine(
input.hasPartialText,
`\u23F1 \u4EFB\u52A1\u8D85\u65F6\uFF1A\u5DF2\u8FBE\u5230\u5355\u6B21\u8FD0\u884C\u65F6\u95F4\u4E0A\u9650\uFF08${input.maxRunSeconds} \u79D2\uFF09\u3002\u8BF7\u62C6\u5206\u4EFB\u52A1\u540E\u91CD\u8BD5\uFF0C\u6216\u8054\u7CFB\u7BA1\u7406\u5458\u8C03\u9AD8\u8FD0\u884C\u65F6\u957F\u4E0A\u9650\u3002`,
),
};
}
if (input.interrupted) {
return { isError: false, notice: undefined };
}
if (input.resultStatus === "completed") {
return { isError: false, notice: undefined };
}
const err = input.resultError ?? "";
if (isMaxTurnsError(err) || input.resultStatus === "length") {
return {
isError: true,
notice: noticeLine(
input.hasPartialText,
`\u26A0\uFE0F \u4EFB\u52A1\u4E2D\u65AD\uFF1A\u5DF2\u8FBE\u5230\u6700\u5927\u6B65\u9AA4\u6570\uFF08${input.maxTurns} \u8F6E\uFF09\u3002\u8BF7\u62C6\u5206\u4EFB\u52A1\u540E\u7EE7\u7EED\uFF0C\u6216\u8054\u7CFB\u7BA1\u7406\u5458\u8C03\u9AD8\u6B65\u9AA4\u4E0A\u9650\u3002`,
),
};
}
if (err.trim() !== "") {
const brief = sanitizeErrorBrief(err);
return {
isError: true,
notice: noticeLine(
input.hasPartialText,
`\u274C \u4EFB\u52A1\u5931\u8D25\uFF1A${brief}\u3002\u8BF7\u91CD\u8BD5\uFF1B\u82E5\u591A\u6B21\u51FA\u73B0\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u3002`,
),
};
}
return {
isError: true,
notice: noticeLine(
input.hasPartialText,
"\u274C \u4EFB\u52A1\u672A\u6B63\u5E38\u5B8C\u6210\u3002\u8BF7\u91CD\u8BD5\uFF1B\u82E5\u591A\u6B21\u51FA\u73B0\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u3002",
),
};
}
export function appendTeacherNotice(body: string, notice: string | undefined): string {
if (notice === undefined || notice === "") return body;
if (body.trim() === "") return notice;
return `${body.trimEnd()}\n\n${notice}`;
}
export function isMaxTurnsError(error: string): boolean {
const lower = error.toLowerCase();
return (
lower.includes("maximum number of turns") ||
lower.includes("max_turns") ||
lower.includes("error_max_turns") ||
lower.includes("result_error_max_turns") ||
(lower.includes("result_error_during_execution") && lower.includes("turn"))
);
}
function noticeLine(hasPartialText: boolean, message: string): string {
if (!hasPartialText) return message;
return `${message}\n\uFF08\u4E0A\u65B9\u4E3A\u5DF2\u751F\u6210\u7684\u90E8\u5206\u7ED3\u679C\u3002\uFF09`;
}
function sanitizeErrorBrief(error: string): string {
const oneLine = error.replace(/\s+/g, " ").trim();
// Drop common SDK prefixes for readability.
const stripped = oneLine
.replace(/^Claude Code returned an error result:\s*/i, "")
.replace(/^Error:\s*/i, "");
if (stripped.length <= 160) return stripped;
return `${stripped.slice(0, 157)}...`;
}