feat(ai): session harness contract — incomplete, run_id, confirm bind
Add docs/16 for session state machine, incomplete/resume, queue semantics, and agent-loop/harness/MCP gap review. Wire RunOutcome on max-rounds, run_id through audit, TrimMessages/Redact hooks, ConfirmBindHash on write confirm path; session file skeleton without HTTP yet. No push.
This commit is contained in:
1 parent
0aeee2b692
commit
5416e86379
17 files changed
+1041
-61
No files matched your search
+20
-3
@@ -43,9 +43,17 @@ function splitContent(content: string): ContentSeg[] {
|
||||
/** R2-F7:等宽代码块 + 顶部「复制」「插入编辑器」小按钮 */
|
||||
function CodeBlock({ code }: { code: string }) {
|
||||
const notify = useWorkspaceStore((s) => s.notify);
|
||||
const notifyError = useWorkspaceStore((s) => s.notifyError);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const copiedTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
useEffect(() => () => { if (copiedTimer.current) clearTimeout(copiedTimer.current); }, []);
|
||||
const copy = async () => {
|
||||
await navigator.clipboard.writeText(code);
|
||||
notify({ kind: "success", title: "已复制", message: "SQL 已复制", mode: "toast", durationMs: 1200 });
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
if (copiedTimer.current) clearTimeout(copiedTimer.current);
|
||||
copiedTimer.current = setTimeout(() => setCopied(false), 1200);
|
||||
} catch (error) { notifyError(error, "toast"); }
|
||||
};
|
||||
const insertToEditor = () => {
|
||||
const st = useWorkspaceStore.getState();
|
||||
@@ -61,7 +69,7 @@ function CodeBlock({ code }: { code: string }) {
|
||||
style={{ borderColor: "var(--ops-border-soft)" }}
|
||||
>
|
||||
<button type="button" className="text-[10px] hover:underline" style={{ color: "var(--ops-accent)" }} onClick={() => void copy()}>
|
||||
复制
|
||||
{copied ? "已复制 ✓" : "复制"}
|
||||
</button>
|
||||
<button type="button" className="text-[10px] hover:underline" style={{ color: "var(--ops-accent)" }} onClick={insertToEditor}>
|
||||
插入编辑器
|
||||
@@ -161,12 +169,16 @@ function DangerConfirmBar({
|
||||
connectionId,
|
||||
database,
|
||||
kind,
|
||||
confirmBind,
|
||||
runId,
|
||||
onDone,
|
||||
}: {
|
||||
sql: string;
|
||||
connectionId?: string;
|
||||
database?: string;
|
||||
kind: "query" | "redis";
|
||||
confirmBind?: string;
|
||||
runId?: string;
|
||||
onDone: (ok: boolean, msg?: string) => void;
|
||||
}) {
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -180,6 +192,8 @@ function DangerConfirmBar({
|
||||
command: kind === "redis" ? sql : undefined,
|
||||
database,
|
||||
kind,
|
||||
confirmBind,
|
||||
runId,
|
||||
});
|
||||
onDone(true);
|
||||
} catch (e) {
|
||||
@@ -528,6 +542,7 @@ export function AiPanel() {
|
||||
content: final.content,
|
||||
steps: final.steps || [],
|
||||
streaming: false,
|
||||
runId: final.runId,
|
||||
});
|
||||
if (final.sql && tab) updateTabSqlIfUnchanged(tab.id, sql, tab.connectionId, tab.database, final.sql);
|
||||
},
|
||||
@@ -674,6 +689,8 @@ export function AiPanel() {
|
||||
connectionId={conn?.id}
|
||||
database={database || undefined}
|
||||
kind={pendingConfirm.step.tool?.includes("redis") ? "redis" : "query"}
|
||||
confirmBind={pendingConfirm.step.confirmBind}
|
||||
runId={m.runId}
|
||||
onDone={(ok, msg) => {
|
||||
if (ok) {
|
||||
patchAiMessage(m.id, (cur) => ({
|
||||
|
||||
@@ -540,6 +540,12 @@ export type AiAgentRes = {
|
||||
sql?: string;
|
||||
steps: AiAgentStep[];
|
||||
rounds: number;
|
||||
runId?: string;
|
||||
sessionId?: string;
|
||||
runOutcome?: string;
|
||||
incompleteReason?: string;
|
||||
resumable?: boolean;
|
||||
stepsBudget?: number;
|
||||
};
|
||||
|
||||
export async function callAiAgent(payload: {
|
||||
@@ -564,6 +570,7 @@ export type AiAgentStepExt = AiAgentStep & {
|
||||
pending?: boolean;
|
||||
needConfirm?: boolean;
|
||||
confirmSql?: string;
|
||||
confirmBind?: string;
|
||||
result?: AiAgentQueryResult;
|
||||
};
|
||||
|
||||
@@ -691,6 +698,8 @@ export async function confirmAiAgentWrite(payload: {
|
||||
command?: string;
|
||||
database?: string;
|
||||
kind?: "query" | "redis";
|
||||
confirmBind?: string;
|
||||
runId?: string;
|
||||
}) {
|
||||
return post<Record<string, unknown>>("/api/ai/agent/confirm", payload);
|
||||
}
|
||||
|
||||
@@ -207,6 +207,7 @@ export type AiAgentStepView = {
|
||||
pending?: boolean;
|
||||
needConfirm?: boolean;
|
||||
confirmSql?: string;
|
||||
confirmBind?: string;
|
||||
result?: AiAgentQueryResultView;
|
||||
};
|
||||
|
||||
@@ -215,6 +216,7 @@ export type AiMessage = {
|
||||
role: "user" | "assistant" | "system";
|
||||
content: string;
|
||||
at: string;
|
||||
runId?: string;
|
||||
/** Agent 工具步骤时间线(仅 assistant) */
|
||||
steps?: AiAgentStepView[];
|
||||
streaming?: boolean;
|
||||
@@ -296,6 +298,8 @@ type State = {
|
||||
detail?: string;
|
||||
tips?: string[];
|
||||
mode?: "modal" | "toast";
|
||||
/** 成功操作默认静默;长任务完成等需要用户注意时显式设为 visible。 */
|
||||
feedback?: "visible";
|
||||
durationMs?: number;
|
||||
raw?: unknown;
|
||||
}) => void;
|
||||
@@ -534,6 +538,7 @@ export const useWorkspaceStore = create<State>((set, get) => ({
|
||||
const parsed = input.raw !== undefined ? formatDbError(input.raw) : null;
|
||||
const kind = input.kind ?? parsed?.kind ?? "info";
|
||||
const mode = input.mode ?? (kind === "error" ? "modal" : "toast");
|
||||
if (kind === "success" && mode === "toast" && input.feedback !== "visible") return;
|
||||
const payload: NotifyPayload = {
|
||||
id: notifyId(),
|
||||
kind,
|
||||
|
||||
Reference in new issue
Block a user