Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8ed295c
Polish AgentForge UI
hetaoBackend Jun 14, 2026
0508a97
Add IM Inbox product spec
hetaoBackend Jun 14, 2026
cf5c6bf
Add IM Inbox task brief foundation
hetaoBackend Jun 14, 2026
45f4fdc
Add Slack IM brief fallback commands
hetaoBackend Jun 14, 2026
7432d63
Add Feishu IM brief fallback commands
hetaoBackend Jun 14, 2026
00ff13f
Add Weixin IM brief fallback commands
hetaoBackend Jun 14, 2026
eb185ce
Add Telegram streaming and brief fallback
hetaoBackend Jun 14, 2026
6cc3e33
Add IM runbooks implementation plan
hetaoBackend Jun 14, 2026
7de9ec8
Add shared IM runbook registry
hetaoBackend Jun 14, 2026
2948e00
Add IM runbook persistence
hetaoBackend Jun 14, 2026
9d280f8
Add scheduler runbook actions
hetaoBackend Jun 14, 2026
f07caed
Add IM runbook API
hetaoBackend Jun 14, 2026
1e68f6a
Add IM runbook text fallback
hetaoBackend Jun 14, 2026
28fbdd9
Complete IM runbooks phase
hetaoBackend Jun 14, 2026
fe0bd3f
Add IM digests implementation plan
hetaoBackend Jun 14, 2026
9901b51
Add IM digest composer
hetaoBackend Jun 14, 2026
80febf6
Add scheduler IM digest action
hetaoBackend Jun 14, 2026
436ce2b
Add IM digest API
hetaoBackend Jun 14, 2026
42d6e21
Complete IM digests phase
hetaoBackend Jun 14, 2026
f0da159
Add IM skill suggestions implementation plan
hetaoBackend Jun 14, 2026
a901435
Add IM skill suggestion renderer
hetaoBackend Jun 14, 2026
17c441f
Persist IM skill suggestion state
hetaoBackend Jun 14, 2026
5460b46
Add IM skill suggestion actions
hetaoBackend Jun 14, 2026
fa02c0b
Add IM skill suggestion text commands
hetaoBackend Jun 14, 2026
3dc4ff0
Format IM skill suggestion changes
hetaoBackend Jun 14, 2026
227d9b4
Minimize Telegram IM changes
hetaoBackend Jun 14, 2026
fea68c5
Pivot IM runbooks to custom commands
hetaoBackend Jun 14, 2026
107fd26
Explain custom commands in IM help
hetaoBackend Jun 14, 2026
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
874 changes: 870 additions & 4 deletions backend/src/api.ts

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions backend/src/bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ export const InboundMessageType = {
RESPOND_TASK: "respond_task", // answer a question a task is waiting on
CANCEL_TASK: "cancel_task", // cancel a task
STATUS_QUERY: "status_query", // query task status
CREATE_BRIEF: "create_brief", // create a draft task brief
CONFIRM_BRIEF: "confirm_brief", // convert a draft brief into a task
DISCARD_BRIEF: "discard_brief", // discard a draft brief
PREVIEW_RUNBOOK: "preview_runbook", // create a draft preview from an IM runbook
RUN_RUNBOOK: "run_runbook", // run an IM runbook or create a confirmation draft
TRIGGER_DIGEST: "trigger_digest", // preview or send an IM digest
SKILL_SUGGESTION_ACTION: "skill_suggestion_action", // draft/show/approve/dismiss a skill suggestion
} as const;
export type InboundMessageType =
(typeof InboundMessageType)[keyof typeof InboundMessageType];
Expand Down Expand Up @@ -65,6 +72,13 @@ function utcNowIso(): string {
* RESPOND_TASK -> {"task_id", "answer"}
* CANCEL_TASK -> {"task_id"}
* STATUS_QUERY -> {"task_id"}
* CREATE_BRIEF -> {"title", "goal", "source_channel", "source_ref", ...}
* CONFIRM_BRIEF -> {"brief_id"}
* DISCARD_BRIEF -> {"brief_id"}
* PREVIEW_RUNBOOK -> {"name", "raw_args", "source_channel", "source_ref", ...}
* RUN_RUNBOOK -> {"name", "raw_args", "source_channel", "source_ref", ...}
* TRIGGER_DIGEST -> {"include_empty", "limit", "since"}
* SKILL_SUGGESTION_ACTION -> {"action", "pattern_id", "source_channel", "target"}
* reply_to: optional reply target (e.g. Feishu chat_id / open_id).
* metadata: channel-specific context (e.g. Feishu message_id).
*/
Expand Down
240 changes: 240 additions & 0 deletions backend/src/channels/brief_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import {
parse_runbook_command,
runbook_from_row,
type ParsedRunbookCommand,
type RunbookDefinition,
} from "../runbooks.ts";

export type { ParsedRunbookCommand } from "../runbooks.ts";

type Row = Record<string, unknown>;
type RunbookDB = {
get_im_runbooks?: (enabled_only?: boolean) => Row[];
};

export type BriefCommand =
| { action: "create"; goal: string }
| { action: "confirm"; brief_id: number }
| { action: "discard"; brief_id: number }
| { action: "help"; reason: "invalid_brief_id" };
type BriefHelpReason = Extract<BriefCommand, { action: "help" }>["reason"];

export type SkillSuggestionCommand =
| { action: "draft" | "show" | "approve" | "dismiss"; pattern_id: number }
| { action: "help"; reason: "invalid_pattern_id" };
type SkillSuggestionHelpReason = Extract<
SkillSuggestionCommand,
{ action: "help" }
>["reason"];

function parseBriefId(value: string): number | null {
const raw = value.trim().replace(/^#+/, "");
if (!/^\d+$/.test(raw)) return null;
const id = Number.parseInt(raw, 10);
return Number.isInteger(id) && id > 0 ? id : null;
}

export function parse_brief_command(text: string): BriefCommand | null {
const trimmed = text.trim();
const match = /^\/([a-z-]+)(?:\s+([\s\S]*))?$/i.exec(trimmed);
if (!match) return null;
const cmd = match[1]!.toLowerCase();
const args = (match[2] ?? "").trim();

if (
cmd === "run-draft" ||
cmd === "confirm-draft" ||
cmd === "confirm-brief" ||
cmd === "run-brief"
) {
const brief_id = parseBriefId(args);
return brief_id === null
? { action: "help", reason: "invalid_brief_id" }
: { action: "confirm", brief_id };
}
if (
cmd === "cancel-draft" ||
cmd === "discard-draft" ||
cmd === "discard-brief"
) {
const brief_id = parseBriefId(args);
return brief_id === null
? { action: "help", reason: "invalid_brief_id" }
: { action: "discard", brief_id };
}
return null;
}

export function parse_skill_suggestion_command(
text: string,
): SkillSuggestionCommand | null {
const trimmed = text.trim();
const match = /^\/([a-z-]+)(?:\s+([\s\S]*))?$/i.exec(trimmed);
if (!match) return null;
const cmd = match[1]!.toLowerCase();
const args = (match[2] ?? "").trim();
const commandToAction: Record<
string,
"draft" | "show" | "approve" | "dismiss"
> = {
"draft-skill": "draft",
"show-skill": "show",
"review-skill": "show",
"approve-skill": "approve",
"dismiss-skill": "dismiss",
};
const action = commandToAction[cmd];
if (!action) return null;
const pattern_id = parseBriefId(args);
return pattern_id === null
? { action: "help", reason: "invalid_pattern_id" }
: { action, pattern_id };
}

function titleFromGoal(goal: string): string {
const singleLine = goal.replace(/\s+/g, " ").trim();
return singleLine.length > 60 ? `${singleLine.slice(0, 57)}...` : singleLine;
}

export function build_brief_payload(args: {
channel: string;
goal: string;
source_ref: string;
source_metadata?: Row;
working_dir?: string | null;
agent?: string | null;
}): Row {
return {
title: titleFromGoal(args.goal),
goal: args.goal.trim(),
context_summary: "",
acceptance_criteria: [],
working_dir: args.working_dir ?? null,
working_dir_confidence: "unknown",
agent: args.agent ?? null,
risk_level: "normal",
needs_confirmation: true,
source_channel: args.channel,
source_ref: args.source_ref,
source_metadata: args.source_metadata ?? {},
};
}

function has_runbook_db(value: unknown): value is RunbookDB {
return (
typeof value === "object" &&
value !== null &&
"get_im_runbooks" in value &&
typeof (value as RunbookDB).get_im_runbooks === "function"
);
}

function runbook_definitions_from_db(db: unknown): RunbookDefinition[] {
if (!has_runbook_db(db)) return [];
try {
const get_im_runbooks = db.get_im_runbooks;
if (!get_im_runbooks) return [];
return get_im_runbooks.call(db, true).map((row) => runbook_from_row(row));
} catch {
return [];
}
}

export function parse_runbook_fallback(
text: string,
db: unknown = null,
): ParsedRunbookCommand | null {
return parse_runbook_command(text, runbook_definitions_from_db(db));
}

export function build_runbook_payload(args: {
channel: string;
command: ParsedRunbookCommand;
source_ref: string;
source_metadata?: Row;
working_dir?: string | null;
agent?: string | null;
}): Row {
return {
name: args.command.name,
raw_args: args.command.raw_args,
source_channel: args.channel,
source_ref: args.source_ref,
source_metadata: args.source_metadata ?? {},
working_dir: args.working_dir ?? null,
agent: args.agent ?? null,
};
}

export function format_brief_help(_reason: BriefHelpReason): string {
return "Usage: `/run-draft <draft_id>` or `/cancel-draft <draft_id>`";
}

export function format_skill_suggestion_help(
_reason: SkillSuggestionHelpReason,
): string {
return "Usage: `/draft-skill <pattern_id>`, `/show-skill <pattern_id>`, `/approve-skill <pattern_id>`, or `/dismiss-skill <pattern_id>`";
}

export function format_skill_suggestion_action_reply(
result: Record<string, unknown>,
): string {
const pattern_id = Number(result["pattern_id"]);
const id = Number.isInteger(pattern_id) && pattern_id > 0 ? pattern_id : "?";
const status = String(result["status"] ?? "");
if (status === "drafting") {
return `Skill draft for pattern #${id} started. Review it with \`/show-skill ${id}\` when ready.`;
}
if (status === "ready") {
return String(result["text"] ?? `Skill draft for pattern #${id} is ready.`);
}
if (status === "approved") {
return `Skill suggestion #${id} approved and installed.`;
}
if (status === "dismissed") {
return `Skill suggestion #${id} dismissed.`;
}
return `Skill suggestion #${id} updated.`;
}

export function format_brief_created_reply(
brief_id: number,
title: string,
): string {
return [
`Draft task #${brief_id}: ${title}`,
"",
`Run: \`/run-draft ${brief_id}\``,
`Cancel: \`/cancel-draft ${brief_id}\``,
].join("\n");
}

export function format_brief_started_reply(
brief_id: number,
task_id: number,
): string {
return `Task #${task_id} created from draft #${brief_id}. Thinking ▌`;
}

export function format_brief_discarded_reply(brief_id: number): string {
return `Draft task #${brief_id} discarded.`;
}

export function format_runbook_created_reply(
task_id: number,
runbook: string,
): string {
return `Command /${runbook} created Task #${task_id}. Thinking ▌`;
}

export function format_runbook_brief_reply(
brief_id: number,
runbook: string,
): string {
return [
`Command /${runbook} created Draft task #${brief_id}.`,
"",
`Run: \`/run-draft ${brief_id}\``,
`Cancel: \`/cancel-draft ${brief_id}\``,
].join("\n");
}
Loading