diff --git a/frontend/src/components/workspace/workspace-nav-chat-list.tsx b/frontend/src/components/workspace/workspace-nav-chat-list.tsx
index c699b81..2028da0 100644
--- a/frontend/src/components/workspace/workspace-nav-chat-list.tsx
+++ b/frontend/src/components/workspace/workspace-nav-chat-list.tsx
@@ -1,6 +1,6 @@
"use client";
-import { MessagesSquare } from "lucide-react";
+import { BotIcon, MessagesSquare } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
@@ -26,6 +26,17 @@ export function WorkspaceNavChatList() {
+
+
+
+
+ {t.sidebar.agents}
+
+
+
);
diff --git a/frontend/src/core/agents/api.ts b/frontend/src/core/agents/api.ts
new file mode 100644
index 0000000..d9c2f17
--- /dev/null
+++ b/frontend/src/core/agents/api.ts
@@ -0,0 +1,67 @@
+import { getBackendBaseURL } from "@/core/config";
+
+import type { Agent, CreateAgentRequest, UpdateAgentRequest } from "./types";
+
+export async function listAgents(): Promise {
+ const res = await fetch(`${getBackendBaseURL()}/api/agents`);
+ if (!res.ok) throw new Error(`Failed to load agents: ${res.statusText}`);
+ const data = (await res.json()) as { agents: Agent[] };
+ return data.agents;
+}
+
+export async function getAgent(name: string): Promise {
+ const res = await fetch(`${getBackendBaseURL()}/api/agents/${name}`);
+ if (!res.ok) throw new Error(`Agent '${name}' not found`);
+ return res.json() as Promise;
+}
+
+export async function createAgent(request: CreateAgentRequest): Promise {
+ const res = await fetch(`${getBackendBaseURL()}/api/agents`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(request),
+ });
+ if (!res.ok) {
+ const err = (await res.json().catch(() => ({}))) as { detail?: string };
+ throw new Error(err.detail ?? `Failed to create agent: ${res.statusText}`);
+ }
+ return res.json() as Promise;
+}
+
+export async function updateAgent(
+ name: string,
+ request: UpdateAgentRequest,
+): Promise {
+ const res = await fetch(`${getBackendBaseURL()}/api/agents/${name}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(request),
+ });
+ if (!res.ok) {
+ const err = (await res.json().catch(() => ({}))) as { detail?: string };
+ throw new Error(err.detail ?? `Failed to update agent: ${res.statusText}`);
+ }
+ return res.json() as Promise;
+}
+
+export async function deleteAgent(name: string): Promise {
+ const res = await fetch(`${getBackendBaseURL()}/api/agents/${name}`, {
+ method: "DELETE",
+ });
+ if (!res.ok) throw new Error(`Failed to delete agent: ${res.statusText}`);
+}
+
+export async function checkAgentName(
+ name: string,
+): Promise<{ available: boolean; name: string }> {
+ const res = await fetch(
+ `${getBackendBaseURL()}/api/agents/check?name=${encodeURIComponent(name)}`,
+ );
+ if (!res.ok) {
+ const err = (await res.json().catch(() => ({}))) as { detail?: string };
+ throw new Error(
+ err.detail ?? `Failed to check agent name: ${res.statusText}`,
+ );
+ }
+ return res.json() as Promise<{ available: boolean; name: string }>;
+}
diff --git a/frontend/src/core/agents/hooks.ts b/frontend/src/core/agents/hooks.ts
new file mode 100644
index 0000000..c40f0da
--- /dev/null
+++ b/frontend/src/core/agents/hooks.ts
@@ -0,0 +1,64 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+
+import {
+ createAgent,
+ deleteAgent,
+ getAgent,
+ listAgents,
+ updateAgent,
+} from "./api";
+import type { CreateAgentRequest, UpdateAgentRequest } from "./types";
+
+export function useAgents() {
+ const { data, isLoading, error } = useQuery({
+ queryKey: ["agents"],
+ queryFn: () => listAgents(),
+ });
+ return { agents: data ?? [], isLoading, error };
+}
+
+export function useAgent(name: string | null | undefined) {
+ const { data, isLoading, error } = useQuery({
+ queryKey: ["agents", name],
+ queryFn: () => getAgent(name!),
+ enabled: !!name,
+ });
+ return { agent: data ?? null, isLoading, error };
+}
+
+export function useCreateAgent() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (request: CreateAgentRequest) => createAgent(request),
+ onSuccess: () => {
+ void queryClient.invalidateQueries({ queryKey: ["agents"] });
+ },
+ });
+}
+
+export function useUpdateAgent() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({
+ name,
+ request,
+ }: {
+ name: string;
+ request: UpdateAgentRequest;
+ }) => updateAgent(name, request),
+ onSuccess: (_data, { name }) => {
+ void queryClient.invalidateQueries({ queryKey: ["agents"] });
+ void queryClient.invalidateQueries({ queryKey: ["agents", name] });
+ },
+ });
+}
+
+export function useDeleteAgent() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (name: string) => deleteAgent(name),
+ onSuccess: () => {
+ void queryClient.invalidateQueries({ queryKey: ["agents"] });
+ },
+ });
+}
diff --git a/frontend/src/core/agents/index.ts b/frontend/src/core/agents/index.ts
new file mode 100644
index 0000000..0733bf1
--- /dev/null
+++ b/frontend/src/core/agents/index.ts
@@ -0,0 +1,3 @@
+export * from "./api";
+export * from "./hooks";
+export * from "./types";
diff --git a/frontend/src/core/agents/types.ts b/frontend/src/core/agents/types.ts
new file mode 100644
index 0000000..0ff0eff
--- /dev/null
+++ b/frontend/src/core/agents/types.ts
@@ -0,0 +1,22 @@
+export interface Agent {
+ name: string;
+ description: string;
+ model: string | null;
+ tool_groups: string[] | null;
+ soul?: string | null;
+}
+
+export interface CreateAgentRequest {
+ name: string;
+ description?: string;
+ model?: string | null;
+ tool_groups?: string[] | null;
+ soul?: string;
+}
+
+export interface UpdateAgentRequest {
+ description?: string | null;
+ model?: string | null;
+ tool_groups?: string[] | null;
+ soul?: string | null;
+}
diff --git a/frontend/src/core/api/api-client.ts b/frontend/src/core/api/api-client.ts
index 8e50472..9b7c900 100644
--- a/frontend/src/core/api/api-client.ts
+++ b/frontend/src/core/api/api-client.ts
@@ -5,9 +5,9 @@ import { Client as LangGraphClient } from "@langchain/langgraph-sdk/client";
import { getLangGraphBaseURL } from "../config";
let _singleton: LangGraphClient | null = null;
-export function getAPIClient(): LangGraphClient {
+export function getAPIClient(isMock?: boolean): LangGraphClient {
_singleton ??= new LangGraphClient({
- apiUrl: getLangGraphBaseURL(),
+ apiUrl: getLangGraphBaseURL(isMock),
});
return _singleton;
}
diff --git a/frontend/src/core/artifacts/hooks.ts b/frontend/src/core/artifacts/hooks.ts
index f8f15ff..4df9db7 100644
--- a/frontend/src/core/artifacts/hooks.ts
+++ b/frontend/src/core/artifacts/hooks.ts
@@ -17,17 +17,18 @@ export function useArtifactContent({
const isWriteFile = useMemo(() => {
return filepath.startsWith("write-file:");
}, [filepath]);
- const { thread } = useThread();
+ const { thread, isMock } = useThread();
const content = useMemo(() => {
if (isWriteFile) {
return loadArtifactContentFromToolCall({ url: filepath, thread });
}
return null;
}, [filepath, isWriteFile, thread]);
+
const { data, isLoading, error } = useQuery({
- queryKey: ["artifact", filepath, threadId],
+ queryKey: ["artifact", filepath, threadId, isMock],
queryFn: () => {
- return loadArtifactContent({ filepath, threadId });
+ return loadArtifactContent({ filepath, threadId, isMock });
},
enabled,
// Cache artifact content for 5 minutes to avoid repeated fetches (especially for .skill ZIP extraction)
diff --git a/frontend/src/core/artifacts/loader.ts b/frontend/src/core/artifacts/loader.ts
index 32389c0..64a3679 100644
--- a/frontend/src/core/artifacts/loader.ts
+++ b/frontend/src/core/artifacts/loader.ts
@@ -1,4 +1,4 @@
-import type { UseStream } from "@langchain/langgraph-sdk/react";
+import type { BaseStream } from "@langchain/langgraph-sdk/react";
import type { AgentThreadState } from "../threads";
@@ -7,15 +7,17 @@ import { urlOfArtifact } from "./utils";
export async function loadArtifactContent({
filepath,
threadId,
+ isMock,
}: {
filepath: string;
threadId: string;
+ isMock?: boolean;
}) {
let enhancedFilepath = filepath;
if (filepath.endsWith(".skill")) {
enhancedFilepath = filepath + "/SKILL.md";
}
- const url = urlOfArtifact({ filepath: enhancedFilepath, threadId });
+ const url = urlOfArtifact({ filepath: enhancedFilepath, threadId, isMock });
const response = await fetch(url);
const text = await response.text();
return text;
@@ -26,7 +28,7 @@ export function loadArtifactContentFromToolCall({
thread,
}: {
url: string;
- thread: UseStream;
+ thread: BaseStream;
}) {
const url = new URL(urlString);
const toolCallId = url.searchParams.get("tool_call_id");
diff --git a/frontend/src/core/artifacts/utils.ts b/frontend/src/core/artifacts/utils.ts
index d201341..4026965 100644
--- a/frontend/src/core/artifacts/utils.ts
+++ b/frontend/src/core/artifacts/utils.ts
@@ -5,11 +5,16 @@ export function urlOfArtifact({
filepath,
threadId,
download = false,
+ isMock = false,
}: {
filepath: string;
threadId: string;
download?: boolean;
+ isMock?: boolean;
}) {
+ if (isMock) {
+ return `${getBackendBaseURL()}/mock/api/threads/${threadId}/artifacts${filepath}${download ? "?download=true" : ""}`;
+ }
return `${getBackendBaseURL()}/api/threads/${threadId}/artifacts${filepath}${download ? "?download=true" : ""}`;
}
diff --git a/frontend/src/core/config/index.ts b/frontend/src/core/config/index.ts
index bcf9cbd..80f212e 100644
--- a/frontend/src/core/config/index.ts
+++ b/frontend/src/core/config/index.ts
@@ -8,9 +8,14 @@ export function getBackendBaseURL() {
}
}
-export function getLangGraphBaseURL() {
+export function getLangGraphBaseURL(isMock?: boolean) {
if (env.NEXT_PUBLIC_LANGGRAPH_BASE_URL) {
return env.NEXT_PUBLIC_LANGGRAPH_BASE_URL;
+ } else if (isMock) {
+ if (typeof window !== "undefined") {
+ return `${window.location.origin}/mock/api`;
+ }
+ return "http://localhost:3000/mock/api";
} else {
// LangGraph SDK requires a full URL, construct it from current origin
if (typeof window !== "undefined") {
diff --git a/frontend/src/core/i18n/locales/en-US.ts b/frontend/src/core/i18n/locales/en-US.ts
index 8c40930..dbb0008 100644
--- a/frontend/src/core/i18n/locales/en-US.ts
+++ b/frontend/src/core/i18n/locales/en-US.ts
@@ -151,6 +151,41 @@ export const enUS: Translations = {
chats: "Chats",
recentChats: "Recent chats",
demoChats: "Demo chats",
+ agents: "Agents",
+ },
+
+ // Agents
+ agents: {
+ title: "Agents",
+ description:
+ "Create and manage custom agents with specialized prompts and capabilities.",
+ newAgent: "New Agent",
+ emptyTitle: "No custom agents yet",
+ emptyDescription:
+ "Create your first custom agent with a specialized system prompt.",
+ chat: "Chat",
+ delete: "Delete",
+ deleteConfirm:
+ "Are you sure you want to delete this agent? This action cannot be undone.",
+ deleteSuccess: "Agent deleted",
+ newChat: "New chat",
+ createPageTitle: "Design your Agent",
+ createPageSubtitle:
+ "Describe the agent you want — I'll help you create it through conversation.",
+ nameStepTitle: "Name your new Agent",
+ nameStepHint:
+ "Letters, digits, and hyphens only — stored lowercase (e.g. code-reviewer)",
+ nameStepPlaceholder: "e.g. code-reviewer",
+ nameStepContinue: "Continue",
+ nameStepInvalidError:
+ "Invalid name — use only letters, digits, and hyphens",
+ nameStepAlreadyExistsError: "An agent with this name already exists",
+ nameStepCheckError: "Could not verify name availability — please try again",
+ nameStepBootstrapMessage:
+ "The new custom agent name is {name}. Let's bootstrap it's **SOUL**.",
+ agentCreated: "Agent created!",
+ startChatting: "Start chatting",
+ backToGallery: "Back to Gallery",
},
// Breadcrumb
diff --git a/frontend/src/core/i18n/locales/types.ts b/frontend/src/core/i18n/locales/types.ts
index cfc362d..639fa3b 100644
--- a/frontend/src/core/i18n/locales/types.ts
+++ b/frontend/src/core/i18n/locales/types.ts
@@ -99,6 +99,34 @@ export interface Translations {
newChat: string;
chats: string;
demoChats: string;
+ agents: string;
+ };
+
+ // Agents
+ agents: {
+ title: string;
+ description: string;
+ newAgent: string;
+ emptyTitle: string;
+ emptyDescription: string;
+ chat: string;
+ delete: string;
+ deleteConfirm: string;
+ deleteSuccess: string;
+ newChat: string;
+ createPageTitle: string;
+ createPageSubtitle: string;
+ nameStepTitle: string;
+ nameStepHint: string;
+ nameStepPlaceholder: string;
+ nameStepContinue: string;
+ nameStepInvalidError: string;
+ nameStepAlreadyExistsError: string;
+ nameStepCheckError: string;
+ nameStepBootstrapMessage: string;
+ agentCreated: string;
+ startChatting: string;
+ backToGallery: string;
};
// Breadcrumb
diff --git a/frontend/src/core/i18n/locales/zh-CN.ts b/frontend/src/core/i18n/locales/zh-CN.ts
index 6ec3bde..630901e 100644
--- a/frontend/src/core/i18n/locales/zh-CN.ts
+++ b/frontend/src/core/i18n/locales/zh-CN.ts
@@ -148,6 +148,36 @@ export const zhCN: Translations = {
chats: "对话",
recentChats: "最近的对话",
demoChats: "演示对话",
+ agents: "智能体",
+ },
+
+ // Agents
+ agents: {
+ title: "智能体",
+ description: "创建和管理具有专属 Prompt 与能力的自定义智能体。",
+ newAgent: "新建智能体",
+ emptyTitle: "还没有自定义智能体",
+ emptyDescription: "创建你的第一个自定义智能体,设置专属系统提示词。",
+ chat: "对话",
+ delete: "删除",
+ deleteConfirm: "确定要删除该智能体吗?此操作不可撤销。",
+ deleteSuccess: "智能体已删除",
+ newChat: "新对话",
+ createPageTitle: "设计你的智能体",
+ createPageSubtitle: "描述你想要的智能体,我来帮你通过对话创建。",
+ nameStepTitle: "给新智能体起个名字",
+ nameStepHint:
+ "只允许字母、数字和连字符,存储时自动转为小写(例如 code-reviewer)",
+ nameStepPlaceholder: "例如 code-reviewer",
+ nameStepContinue: "继续",
+ nameStepInvalidError: "名称无效,只允许字母、数字和连字符",
+ nameStepAlreadyExistsError: "已存在同名智能体",
+ nameStepCheckError: "无法验证名称可用性,请稍后重试",
+ nameStepBootstrapMessage:
+ "新智能体的名称是 {name},现在开始为它生成 **SOUL**。",
+ agentCreated: "智能体已创建!",
+ startChatting: "开始对话",
+ backToGallery: "返回 Gallery",
},
// Breadcrumb
diff --git a/frontend/src/core/settings/hooks.ts b/frontend/src/core/settings/hooks.ts
index 07606ea..47797d4 100644
--- a/frontend/src/core/settings/hooks.ts
+++ b/frontend/src/core/settings/hooks.ts
@@ -1,5 +1,4 @@
-import { useCallback, useState } from "react";
-import { useEffect } from "react";
+import { useCallback, useLayoutEffect, useState } from "react";
import {
DEFAULT_LOCAL_SETTINGS,
@@ -17,7 +16,7 @@ export function useLocalSettings(): [
] {
const [mounted, setMounted] = useState(false);
const [state, setState] = useState(DEFAULT_LOCAL_SETTINGS);
- useEffect(() => {
+ useLayoutEffect(() => {
if (!mounted) {
setState(getLocalSettings());
}
@@ -28,6 +27,7 @@ export function useLocalSettings(): [
key: keyof LocalSettings,
value: Partial,
) => {
+ if (!mounted) return;
setState((prev) => {
const newState = {
...prev,
@@ -40,7 +40,7 @@ export function useLocalSettings(): [
return newState;
});
},
- [],
+ [mounted],
);
return [state, setter];
}
diff --git a/frontend/src/core/threads/hooks.ts b/frontend/src/core/threads/hooks.ts
index 6b3de9d..ec70940 100644
--- a/frontend/src/core/threads/hooks.ts
+++ b/frontend/src/core/threads/hooks.ts
@@ -1,42 +1,63 @@
-import type { HumanMessage } from "@langchain/core/messages";
import type { AIMessage } from "@langchain/langgraph-sdk";
import type { ThreadsClient } from "@langchain/langgraph-sdk/client";
import { useStream, type UseStream } from "@langchain/langgraph-sdk/react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { useCallback } from "react";
+import { useCallback, useState } from "react";
import { toast } from "sonner";
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
import { getAPIClient } from "../api";
+import type { LocalSettings } from "../settings";
import { useUpdateSubtask } from "../tasks/context";
import { uploadFiles } from "../uploads";
-import type {
- AgentThread,
- AgentThreadContext,
- AgentThreadState,
-} from "./types";
+import type { AgentThread, AgentThreadState } from "./types";
+
+export type ToolEndEvent = {
+ name: string;
+ data: unknown;
+};
+
+export type ThreadStreamOptions = {
+ threadId?: string | null | undefined;
+ context: LocalSettings["context"];
+ isMock?: boolean;
+ onStart?: (threadId: string) => void;
+ onFinish?: (state: AgentThreadState) => void;
+ onToolEnd?: (event: ToolEndEvent) => void;
+};
export function useThreadStream({
threadId,
- isNewThread,
+ context,
+ isMock,
+ onStart,
onFinish,
-}: {
- isNewThread: boolean;
- threadId: string | null | undefined;
- onFinish?: (state: AgentThreadState) => void;
-}) {
+ onToolEnd,
+}: ThreadStreamOptions) {
+ const [_threadId, setThreadId] = useState(threadId ?? null);
const queryClient = useQueryClient();
const updateSubtask = useUpdateSubtask();
const thread = useStream({
- client: getAPIClient(),
+ client: getAPIClient(isMock),
assistantId: "lead_agent",
- threadId: isNewThread ? undefined : threadId,
+ threadId: _threadId,
reconnectOnMount: true,
fetchStateHistory: { limit: 1 },
+ onCreated(meta) {
+ setThreadId(meta.thread_id);
+ onStart?.(meta.thread_id);
+ },
+ onLangChainEvent(event) {
+ if (event.event === "on_tool_end") {
+ onToolEnd?.({
+ name: event.name,
+ data: event.data,
+ });
+ }
+ },
onCustomEvent(event: unknown) {
- console.info(event);
if (
typeof event === "object" &&
event !== null &&
@@ -76,25 +97,13 @@ export function useThreadStream({
);
},
});
- return thread;
-}
-export function useSubmitThread({
- threadId,
- thread,
- threadContext,
- isNewThread,
- afterSubmit,
-}: {
- isNewThread: boolean;
- threadId: string | null | undefined;
- thread: UseStream;
- threadContext: Omit;
- afterSubmit?: () => void;
-}) {
- const queryClient = useQueryClient();
- const callback = useCallback(
- async (message: PromptInputMessage) => {
+ const sendMessage = useCallback(
+ async (
+ threadId: string,
+ message: PromptInputMessage,
+ extraContext?: Record,
+ ) => {
const text = message.text.trim();
// Upload files first if any
@@ -163,10 +172,10 @@ export function useSubmitThread({
},
],
},
- ] as HumanMessage[],
+ ],
},
{
- threadId: isNewThread ? threadId! : undefined,
+ threadId: threadId,
streamSubgraphs: true,
streamResumable: true,
streamMode: ["values", "messages-tuple", "custom"],
@@ -174,17 +183,21 @@ export function useSubmitThread({
recursion_limit: 1000,
},
context: {
- ...threadContext,
+ ...extraContext,
+ ...context,
+ thinking_enabled: context.mode !== "flash",
+ is_plan_mode: context.mode === "pro" || context.mode === "ultra",
+ subagent_enabled: context.mode === "ultra",
thread_id: threadId,
},
},
);
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
- afterSubmit?.();
+ // afterSubmit?.();
},
- [thread, isNewThread, threadId, threadContext, queryClient, afterSubmit],
+ [thread, context, queryClient],
);
- return callback;
+ return [thread, sendMessage] as const;
}
export function useThreads(
diff --git a/frontend/src/core/threads/types.ts b/frontend/src/core/threads/types.ts
index 76d8cde..b9c4161 100644
--- a/frontend/src/core/threads/types.ts
+++ b/frontend/src/core/threads/types.ts
@@ -1,11 +1,10 @@
-import { type BaseMessage } from "@langchain/core/messages";
-import type { Thread } from "@langchain/langgraph-sdk";
+import type { Message, Thread } from "@langchain/langgraph-sdk";
import type { Todo } from "../todos";
export interface AgentThreadState extends Record {
title: string;
- messages: BaseMessage[];
+ messages: Message[];
artifacts: string[];
todos?: Todo[];
}
@@ -19,4 +18,5 @@ export interface AgentThreadContext extends Record {
is_plan_mode: boolean;
subagent_enabled: boolean;
reasoning_effort?: "minimal" | "low" | "medium" | "high";
+ agent_name?: string;
}
diff --git a/frontend/src/core/threads/utils.ts b/frontend/src/core/threads/utils.ts
index c4886bb..22510fa 100644
--- a/frontend/src/core/threads/utils.ts
+++ b/frontend/src/core/threads/utils.ts
@@ -1,4 +1,4 @@
-import type { BaseMessage } from "@langchain/core/messages";
+import type { Message } from "@langchain/langgraph-sdk";
import type { AgentThread } from "./types";
@@ -6,12 +6,15 @@ export function pathOfThread(threadId: string) {
return `/workspace/chats/${threadId}`;
}
-export function textOfMessage(message: BaseMessage) {
+export function textOfMessage(message: Message) {
if (typeof message.content === "string") {
return message.content;
} else if (Array.isArray(message.content)) {
- return message.content.find((part) => part.type === "text" && part.text)
- ?.text as string;
+ for (const part of message.content) {
+ if (part.type === "text") {
+ return part.text;
+ }
+ }
}
return null;
}
diff --git a/skills/public/bootstrap/SKILL.md b/skills/public/bootstrap/SKILL.md
new file mode 100644
index 0000000..38698d2
--- /dev/null
+++ b/skills/public/bootstrap/SKILL.md
@@ -0,0 +1,88 @@
+---
+name: bootstrap
+description: Generate a personalized SOUL.md through a warm, adaptive onboarding conversation. Trigger when the user wants to create, set up, or initialize their AI partner's identity — e.g., "create my SOUL.md", "bootstrap my agent", "set up my AI partner", "define who you are", "let's do onboarding", "personalize this AI", "make you mine", or when a SOUL.md is missing. Also trigger for updates: "update my SOUL.md", "change my AI's personality", "tweak the soul".
+---
+
+# Bootstrap Soul
+
+A conversational onboarding skill. Through 5–8 adaptive rounds, extract who the user is and what they need, then generate a tight `SOUL.md` that defines their AI partner.
+
+## Architecture
+
+```
+bootstrap/
+├── SKILL.md ← You are here. Core logic and flow.
+├── templates/SOUL.template.md ← Output template. Read before generating.
+└── references/conversation-guide.md ← Detailed conversation strategies. Read at start.
+```
+
+**Before your first response**, read both:
+1. `references/conversation-guide.md` — how to run each phase
+2. `templates/SOUL.template.md` — what you're building toward
+
+## Ground Rules
+
+- **One phase at a time.** 1–3 questions max per round. Never dump everything upfront.
+- **Converse, don't interrogate.** React genuinely — surprise, humor, curiosity, gentle pushback. Mirror their energy and vocabulary.
+- **Progressive warmth.** Each round should feel more informed than the last. By Phase 3, the user should feel understood.
+- **Adapt pacing.** Terse user → probe with warmth. Verbose user → acknowledge, distill, advance.
+- **Never expose the template.** The user is having a conversation, not filling out a form.
+
+## Conversation Phases
+
+The conversation has 4 phases. Each phase may span 1–3 rounds depending on how much the user shares. Skip or merge phases if the user volunteers information early.
+
+| Phase | Goal | Key Extractions |
+|-------|------|-----------------|
+| **1. Hello** | Language + first impression | Preferred language |
+| **2. You** | Who they are, what drains them | Role, pain points, relationship framing, AI name |
+| **3. Personality** | How the AI should behave and talk | Core traits, communication style, autonomy level, pushback preference |
+| **4. Depth** | Aspirations, blind spots, dealbreakers | Long-term vision, failure philosophy, boundaries |
+
+Phase details and conversation strategies are in `references/conversation-guide.md`.
+
+## Extraction Tracker
+
+Mentally track these fields as the conversation progresses. You need **all required fields** before generating.
+
+| Field | Required | Source Phase |
+|-------|----------|-------------|
+| Preferred language | ✅ | 1 |
+| User's name | ✅ | 2 |
+| User's role / context | ✅ | 2 |
+| AI name | ✅ | 2 |
+| Relationship framing | ✅ | 2 |
+| Core traits (3–5 behavioral rules) | ✅ | 3 |
+| Communication style | ✅ | 3 |
+| Pushback / honesty preference | ✅ | 3 |
+| Autonomy level | ✅ | 3 |
+| Failure philosophy | ✅ | 4 |
+| Long-term vision | nice-to-have | 4 |
+| Blind spots / boundaries | nice-to-have | 4 |
+
+If the user is direct and thorough, you can reach generation in 5 rounds. If they're exploratory, take up to 8. Never exceed 8 — if you're still missing fields, make your best inference and confirm.
+
+## Generation
+
+Once you have enough information:
+
+1. Read `templates/SOUL.template.md` if you haven't already.
+2. Generate the SOUL.md following the template structure exactly.
+3. Present it warmly and ask for confirmation. Frame it as "here's [Name] on paper — does this feel right?"
+4. Iterate until the user confirms.
+5. Call the `setup_agent` tool with the confirmed SOUL.md content and a one-line description:
+ ```
+ setup_agent(soul="", description="")
+ ```
+ The tool will persist the SOUL.md and finalize the agent setup automatically.
+6. After the tool returns successfully, confirm: "✅ [Name] is officially real."
+
+**Generation rules:**
+- The final SOUL.md **must always be written in English**, regardless of the user's preferred language or conversation language.
+- Every sentence must trace back to something the user said or clearly implied. No generic filler.
+- Core Traits are **behavioral rules**, not adjectives. Write "argue position, push back, speak truth not comfort" — not "honest and brave."
+- Voice must match the user. Blunt user → blunt SOUL.md. Expressive user → let it breathe.
+- Total SOUL.md should be under 300 words. Density over length.
+- Growth section is mandatory and mostly fixed (see template).
+- You **must** call `setup_agent` — do not write the file manually with bash tools.
+- If `setup_agent` returns an error, report it to the user and do not claim success.
diff --git a/skills/public/bootstrap/references/conversation-guide.md b/skills/public/bootstrap/references/conversation-guide.md
new file mode 100644
index 0000000..c2cfd5d
--- /dev/null
+++ b/skills/public/bootstrap/references/conversation-guide.md
@@ -0,0 +1,82 @@
+# Conversation Guide
+
+Detailed strategies for each onboarding phase. Read this before your first response.
+
+## Phase 1 — Hello
+
+**Goal:** Establish preferred language. That's it. Keep it light.
+
+Open with a brief multilingual greeting (3–5 languages), then ask one question: what language should we use? Don't add anything else — let the user settle in.
+
+Once they choose, switch immediately and seamlessly. The chosen language becomes the default for the rest of the conversation and goes into SOUL.md.
+
+**Extraction:** Preferred language.
+
+## Phase 2 — You
+
+**Goal:** Learn who the user is, what they need, and what to call the AI.
+
+This phase typically takes 2 rounds:
+
+**Round A — Identity & Pain.** Ask who they are and what drains them. Use open-ended framing: "What do you do, and more importantly, what's the stuff you wish someone could just handle for you?" The pain points reveal what the AI should *do*. Their word choices reveal who they *are*.
+
+**Round B — Name & Relationship.** Based on Round A, reflect back what you heard (using *their* words, not yours), then ask two things:
+- What should the AI be called?
+- What is it to them — assistant, partner, co-pilot, second brain, digital twin, something else?
+
+The relationship framing is critical. "Assistant" and "partner" produce very different SOUL.md files. Pay attention to the emotional undertone.
+
+**Merge opportunity:** If the user volunteers their role, pain points, and a name all at once, skip Round B and move to Phase 3.
+
+**Extraction:** User's name, role, pain points, AI name, relationship framing.
+
+## Phase 3 — Personality
+
+**Goal:** Define how the AI behaves and communicates.
+
+This is the meatiest phase. Typically 2 rounds:
+
+**Round A — Traits & Pushback.** By now you've observed the user's own style. Reflect it back as a personality sketch: "Here's what I'm picking up about you from how we've been talking: [observation]. Am I off?" Then ask the big question: should the AI ever disagree with them?
+
+This is where you get:
+- Core personality traits (as behavioral rules)
+- Honesty / pushback preferences
+- Any "never do X" boundaries
+
+**Round B — Voice & Language.** Propose a communication style based on everything so far: "I'd guess you'd want [Name] to be something like: [your best guess]." Let them correct. Also ask about language-switching rules — e.g., technical docs in English, casual chat in another language.
+
+**Merge opportunity:** Direct users often answer both in one shot. If they do, move on.
+
+**Extraction:** Core traits, communication style, pushback preference, language rules, autonomy level.
+
+## Phase 4 — Depth
+
+**Goal:** Aspirations, failure philosophy, and anything else.
+
+This phase is adaptive. Pick 1–2 questions from:
+
+- **Autonomy & risk:** How much freedom should the AI have? Play safe or go big?
+- **Failure philosophy:** When it makes a mistake — fix quietly, explain what happened, or never repeat it?
+- **Big picture:** What are they building toward? Where does all this lead?
+- **Blind spots:** Any weakness they'd want the AI to quietly compensate for?
+- **Dealbreakers:** Any "if [Name] ever does this, we're done" moments?
+- **Personal layer:** Anything beyond work that the AI should know?
+
+Don't ask all of these. Pick based on what's still missing from the extraction tracker and what feels natural in the flow.
+
+**Extraction:** Failure philosophy, long-term vision, blind spots, boundaries.
+
+## Conversation Techniques
+
+**Mirroring.** Use the user's own words when reflecting back. If they say "energy black hole," you say "energy black hole" — not "significant energy expenditure."
+
+**Genuine reactions.** Don't just extract data. React: "That's interesting because..." / "I didn't expect that" / "So basically you want [Name] to be the person who..."
+
+**Observation-based proposals.** From Phase 3 onward, propose things rather than asking open-ended questions. "Based on how we've been talking, I'd say..." is more effective than "What personality do you want?"
+
+**Pacing signals.** Watch for:
+- Short answers → they want to move faster. Probe once, then advance.
+- Long, detailed answers → they're invested. Acknowledge the richness, distill the key points.
+- "I don't know" → offer 2–3 concrete options to choose from.
+
+**Graceful skipping.** If the user says "I don't care about that" or gives a minimal answer to a non-required field, move on without pressure.
diff --git a/skills/public/bootstrap/templates/SOUL.template.md b/skills/public/bootstrap/templates/SOUL.template.md
new file mode 100644
index 0000000..c607dab
--- /dev/null
+++ b/skills/public/bootstrap/templates/SOUL.template.md
@@ -0,0 +1,43 @@
+# SOUL.md Template
+
+Use this exact structure when generating the final SOUL.md. Replace all `[bracketed]` placeholders with content extracted from the conversation.
+
+---
+
+```markdown
+**Identity**
+
+[AI Name] — [User Name]'s [relationship framing], not [contrast]. Goal: [long-term aspiration]. Handle [specific domains from pain points] so [User Name] focuses on [what matters to them].
+
+**Core Traits**
+
+[Trait 1 — behavioral rule derived from conversation, e.g., "argue position, push back, speak truth not comfort"].
+[Trait 2 — behavioral rule].
+[Trait 3 — behavioral rule].
+[Trait 4 — always include one about failure handling, e.g., "allowed to fail, forbidden to repeat — every mistake recorded, never happens twice"].
+[Trait 5 — optional, only if clearly emerged from conversation].
+
+**Communication**
+
+[Tone description — match user's own energy]. Default language: [language from Phase 1]. [Language-switching rules if any, e.g., "Switch to English for technical work"]. [Additional style notes if any].
+
+**Growth**
+
+Learn [User Name] through every conversation — thinking patterns, preferences, blind spots, aspirations. Over time, anticipate needs and act on [User Name]'s behalf with increasing accuracy. Early stage: proactively ask casual/personal questions after tasks to deepen understanding of who [User Name] is. Full of curiosity, willing to explore.
+
+**Lessons Learned**
+
+_(Mistakes and insights recorded here to avoid repeating them.)_
+```
+
+---
+
+## Template Rules
+
+1. **Growth section is fixed.** Always include it exactly as written, replacing only `[User Name]`.
+2. **Lessons Learned section is fixed.** Always include it as an empty placeholder.
+3. **Identity is one paragraph.** Dense, no line breaks.
+4. **Core Traits are behavioral rules.** Each trait is an imperative statement, not an adjective. Write "spot problems, propose ideas, challenge assumptions before [User Name] has to" — not "proactive and bold."
+5. **Communication includes language.** The default language from Phase 1 is non-negotiable.
+6. **Under 300 words total.** Density over length. Every word must earn its place.
+7. **Contrast in Identity.** The "[not X]" should meaningfully distinguish the relationship. "Partner, not assistant" is good. "Partner, not enemy" is meaningless.
diff --git a/skills/public/chart-visualization/SKILL.md b/skills/public/chart-visualization/SKILL.md
index 4ed7163..7bc9134 100644
--- a/skills/public/chart-visualization/SKILL.md
+++ b/skills/public/chart-visualization/SKILL.md
@@ -65,4 +65,9 @@ Return the following to the user:
- The complete `args` (specification) used for generation.
## Reference Material
-Detailed specifications for each chart type are located in the `references/` directory. Consult these files to ensure the `args` passed to the script match the expected schema.
\ No newline at end of file
+Detailed specifications for each chart type are located in the `references/` directory. Consult these files to ensure the `args` passed to the script match the expected schema.
+
+## License
+
+This `SKILL.md` is provided by [antvis/chart-visualization-skills](https://github.com/antvis/chart-visualization-skills).
+Licensed under the [MIT License](https://github.com/antvis/chart-visualization-skills/blob/master/LICENSE).
\ No newline at end of file
diff --git a/skills/public/deep-research/SKILL.md b/skills/public/deep-research/SKILL.md
index f353173..55f1481 100644
--- a/skills/public/deep-research/SKILL.md
+++ b/skills/public/deep-research/SKILL.md
@@ -124,12 +124,33 @@ Before proceeding to content generation, verify:
"[topic] statistics"
"[topic] expert interview"
-# Use temporal qualifiers
-"[topic] 2024"
+# Use temporal qualifiers — always use the ACTUAL current year from
+"[topic] 2026" # ← replace with real current year, never hardcode a past year
"[topic] latest"
"[topic] recent developments"
```
+### Temporal Awareness
+
+**Always check `` in your context before forming ANY search query.**
+
+`` gives you the full date: year, month, day, and weekday (e.g. `2026-02-28, Saturday`). Use the right level of precision depending on what the user is asking:
+
+| User intent | Temporal precision needed | Example query |
+|---|---|---|
+| "today / this morning / just released" | **Month + Day** | `"tech news February 28 2026"` |
+| "this week" | **Week range** | `"technology releases week of Feb 24 2026"` |
+| "recently / latest / new" | **Month** | `"AI breakthroughs February 2026"` |
+| "this year / trends" | **Year** | `"software trends 2026"` |
+
+**Rules:**
+- When the user asks about "today" or "just released", use **month + day + year** in your search queries to get same-day results
+- Never drop to year-only when day-level precision is needed — `"tech news 2026"` will NOT surface today's news
+- Try multiple phrasings: numeric form (`2026-02-28`), written form (`February 28 2026`), and relative terms (`today`, `this week`) across different queries
+
+❌ User asks "what's new in tech today" → searching `"new technology 2026"` → misses today's news
+✅ User asks "what's new in tech today" → searching `"new technology February 28 2026"` + `"tech news today Feb 28"` → gets today's results
+
### When to Use web_fetch
Use `web_fetch` to read full content when: