package vip.mate.agent; // PR-0b: DashScope imports moved with the construction code into DashScopeChatModelBuilder. import com.alibaba.cloud.ai.graph.CompiledGraph; import com.alibaba.cloud.ai.graph.CompileConfig; import com.alibaba.cloud.ai.graph.KeyStrategy; import com.alibaba.cloud.ai.graph.KeyStrategyFactory; import com.alibaba.cloud.ai.graph.StateGraph; import com.alibaba.cloud.ai.graph.action.AsyncEdgeAction; import com.alibaba.cloud.ai.graph.action.AsyncNodeAction; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.tool.ToolCallback; import org.springframework.retry.support.RetryTemplate; import org.springframework.stereotype.Component; import vip.mate.agent.graph.StateGraphReActAgent; import vip.mate.agent.graph.NodeStreamingChatHelper; import vip.mate.agent.graph.executor.ToolExecutionExecutor; import vip.mate.agent.graph.edge.ObservationDispatcher; import vip.mate.agent.graph.edge.ReasoningDispatcher; import vip.mate.agent.graph.lifecycle.ReActLifecycleListener; import vip.mate.agent.graph.node.*; import vip.mate.agent.graph.state.MateClawStateAccessor; import vip.mate.agent.graph.observation.ObservationProcessor; import vip.mate.agent.graph.plan.StateGraphPlanExecuteAgent; import vip.mate.agent.graph.plan.edge.PlanGenerationDispatcher; import vip.mate.agent.graph.plan.edge.StepProgressDispatcher; import vip.mate.agent.graph.plan.node.*; import vip.mate.agent.graph.plan.state.PlanStateKeys; import vip.mate.agent.graph.state.MateClawStateKeys; import vip.mate.agent.binding.service.AgentBindingService; import vip.mate.agent.model.AgentEntity; import org.springframework.beans.factory.annotation.Autowired; import vip.mate.config.GraphObservationProperties; import vip.mate.config.ReasoningRetentionProperties; import vip.mate.exception.MateClawException; import vip.mate.llm.chatmodel.HttpTimeouts; import vip.mate.llm.chatmodel.OpenAiCompatibleChatModelBuilder; import vip.mate.llm.chatmodel.ProviderGenerateKwargs; import vip.mate.llm.chatmodel.ReasoningEffortResolver; import vip.mate.llm.model.ModelConfigEntity; import vip.mate.llm.model.ModelFamily; import vip.mate.llm.model.ModelProtocol; import vip.mate.agent.context.PrefixBudgetPlan; import vip.mate.agent.context.PrefixBudgetPlanner; import vip.mate.agent.context.TokenEstimator; import vip.mate.llm.model.ModelProviderEntity; import vip.mate.llm.probe.ModelContextWindowResolver; import vip.mate.llm.routing.ProviderModelRef; import vip.mate.llm.routing.ProviderRouter; import vip.mate.llm.service.ModelConfigService; import vip.mate.llm.service.ModelProviderService; import vip.mate.planning.service.PlanningService; import vip.mate.skill.runtime.SkillCatalogRenderer; import vip.mate.skill.service.SkillService; import vip.mate.system.service.SystemSettingService; import vip.mate.tool.ToolRegistry; import vip.mate.tool.disclosure.ToolUsageRecencyTracker; import vip.mate.tool.mcp.runtime.McpProgressContext; import vip.mate.memory.spi.MemoryManager; import vip.mate.workspace.document.WorkspaceFileService; import vip.mate.tool.guard.service.ToolGuardService; import vip.mate.workspace.conversation.ConversationService; import vip.mate.approval.ApprovalWorkflowService; import vip.mate.channel.web.ChatStreamTracker; import vip.mate.team.service.TeamContextBuilder; import vip.mate.team.service.TeamPlanBridge; import vip.mate.wiki.service.WikiContextService; import java.lang.reflect.Field; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * Agent 图构建器 *
* 纯构建器,不做执行。从 AgentService 中提取出所有 Agent 实例构建逻辑, * 包括模型创建、图编译、prompt 增强等。 * * @author MateClaw Team */ @Slf4j @Component @RequiredArgsConstructor public class AgentGraphBuilder { private final ToolRegistry toolRegistry; private final AgentBindingService agentBindingService; private final SkillService skillService; private final vip.mate.skill.runtime.SkillRuntimeService skillRuntimeService; private final vip.mate.tool.disclosure.ToolDisclosureService toolDisclosureService; private final vip.mate.agent.progress.ProgressLedgerService progressLedgerService; /** Escape hatch: when false, the load_skill meta tool is not advertised. */ @org.springframework.beans.factory.annotation.Value( "${mateclaw.skill.disclosure.load-skill-tool.enabled:true}") private boolean loadSkillToolEnabled; /** Escape hatch: when false, the final answer is sent verbatim without Markdown normalization. */ @org.springframework.beans.factory.annotation.Value( "${mate.agent.markdown-normalize-enabled:true}") private boolean markdownNormalizeEnabled; private final ConversationService conversationService; private final TeamContextBuilder teamContextBuilder; private final TeamPlanBridge teamPlanBridge; private final ModelConfigService modelConfigService; private final ModelProviderService modelProviderService; private final ModelContextWindowResolver contextWindowResolver; private final PrefixBudgetPlanner prefixBudgetPlanner; private final ToolUsageRecencyTracker toolUsageRecencyTracker; private final McpProgressContext progressContext; private final vip.mate.llm.service.ModelCapabilityService modelCapabilityService; private final ProviderRouter providerRouter; private final PlanningService planningService; private final ToolGuardService toolGuardService; private final vip.mate.tool.guard.service.ToolGuardConfigService toolGuardConfigService; private final ApprovalWorkflowService approvalService; private final ChatStreamTracker streamTracker; private final SystemSettingService systemSettingService; // PR-0b: dashScopeChatModel + dashScopeConnectionProperties live on DashScopeChatModelBuilder now. private final RetryTemplate retryTemplate; private final GraphObservationProperties graphObservationProperties; private final vip.mate.config.ToolTimeoutProperties toolTimeoutProperties; private final MemoryManager memoryManager; private final WorkspaceFileService workspaceFileService; private final vip.mate.agent.context.ConversationWindowManager conversationWindowManager; private final vip.mate.llm.chatgpt.ChatGPTResponsesClient chatGPTResponsesClient; private final WikiContextService wikiContextService; private final vip.mate.workspace.core.service.WorkspaceService workspaceService; private final vip.mate.llm.cache.AnthropicCacheOptionsFactory anthropicCacheOptionsFactory; private final vip.mate.llm.cache.LlmCacheMetricsAggregator llmCacheMetricsAggregator; private final vip.mate.agent.graph.executor.ToolResultStorage toolResultStorage; private final vip.mate.tool.ToolConcurrencyRegistry toolConcurrencyRegistry; private final vip.mate.i18n.I18nService i18nService; private final vip.mate.llm.failover.ProviderHealthTracker providerHealthTracker; private final vip.mate.llm.chatmodel.ProviderChatModelFactory chatModelFactory; private final vip.mate.llm.failover.AvailableProviderPool providerPool; private final vip.mate.tool.document.GeneratedFileCache generatedFileCache; /** DashScope-specific construction lives here; only called for the built-in-search log. */ private final vip.mate.llm.chatmodel.DashScopeChatModelBuilder dashScopeBuilder; private final vip.mate.llm.routing.MultimodalRouter multimodalRouter; private final vip.mate.llm.routing.MediaCaptionService mediaCaptionService; private final vip.mate.goal.service.GoalService goalService; private final vip.mate.goal.service.GoalEvaluationService goalEvaluationService; private final vip.mate.goal.service.GoalFollowupService goalFollowupService; private final vip.mate.goal.config.GoalProperties goalProperties; /** C4: per-conversation environment notification registry, injected into ReasoningNode. */ private final vip.mate.agent.runtime.RunningConversationRegistry runningConversationRegistry; /** * Auto-grant resolver wired into the executor so an active * {@code mate_approval_grant} row can skip {@code createPending()} for matching * tool calls. Together with {@link #workspaceLookupCache}, these two deps form * the auto-grant entry point; the executor's null-guard turns the feature off * cleanly if either is missing. */ private final vip.mate.approval.grant.service.ApprovalGrantResolver approvalGrantResolver; /** Conversation→workspaceId lookup cache; see {@link #approvalGrantResolver}. */ private final vip.mate.approval.grant.WorkspaceLookupCache workspaceLookupCache; /** * Optional audit pipeline. Setter injection (rather than a constructor * parameter) keeps existing constructor-based wiring + tests intact. * When present, the executor receives it so child-agent denied-tool * attempts can be recorded. */ private vip.mate.audit.service.AuditEventService auditEventService; @Autowired(required = false) public void setAuditEventService(vip.mate.audit.service.AuditEventService s) { this.auditEventService = s; } /** * Reasoning retention policy for ReAct turns. Setter injection so the * {@code @RequiredArgsConstructor} signature stays stable for the unit * constructions across the test suite; null in those, where the agent's own * default (keep every iteration) applies. */ private ReasoningRetentionProperties reasoningRetentionProperties; @Autowired(required = false) public void setReasoningRetentionProperties(ReasoningRetentionProperties p) { this.reasoningRetentionProperties = p; } /** * Optional per-step delegation dependencies for the Plan-Execute graph. * Setter injection (like {@link #auditEventService}) breaks the * {@code AgentService ⇆ AgentGraphBuilder} construction cycle. Null when not * wired (legacy / test) — per-step delegation is then simply disabled. */ private AgentService agentService; // @Lazy on the injection point: inject a lazy-resolution proxy so the // AgentService ⇆ AgentGraphBuilder cycle is broken at bean-creation time // (the real bean is resolved on first use, when the graph is built). @org.springframework.beans.factory.annotation.Autowired(required = false) public void setAgentService(@org.springframework.context.annotation.Lazy AgentService agentService) { this.agentService = agentService; } private vip.mate.tool.builtin.DelegateAgentTool delegateAgentTool; @org.springframework.beans.factory.annotation.Autowired(required = false) public void setDelegateAgentTool( @org.springframework.context.annotation.Lazy vip.mate.tool.builtin.DelegateAgentTool delegateAgentTool) { this.delegateAgentTool = delegateAgentTool; } /** * 根据 AgentEntity 构建完整的 Agent 实例(沿用 Agent / 全局默认模型)。 */ public BaseAgent build(AgentEntity entity) { return build(entity, null, null); } /** * Resolve the model the runtime should use, honouring the precedence * conversation pin > Agent model override > global default. * A conversation pin that no longer resolves to an enabled model (the model * was disabled or deleted after it was picked) silently degrades to the * Agent / global default rather than failing the chat. */ private ModelConfigEntity resolveRuntimeBaseModel(String modelProvider, String modelName, String agentModelName) { if (modelProvider != null && !modelProvider.isBlank() && modelName != null && !modelName.isBlank()) { ModelConfigEntity pinned = modelConfigService.findEnabledModel(modelProvider, modelName); if (pinned != null) { return pinned; } log.info("Conversation model pin {}/{} is no longer an enabled model — " + "falling back to the Agent / global default", modelProvider, modelName); } return modelConfigService.resolveModel(agentModelName); } /** * True iff the caller passed a complete (provider, model) pin AND that * pair resolves to an enabled model row. Used by {@link #build} to decide * whether the explicit pick should bypass capability-driven routing. */ private boolean pinResolvesToEnabledModel(String modelProvider, String modelName) { if (modelProvider == null || modelProvider.isBlank() || modelName == null || modelName.isBlank()) { return false; } try { return modelConfigService.findEnabledModel(modelProvider, modelName) != null; } catch (Exception e) { return false; } } /** * True when the Agent declared its own modelName and that name resolved to * a real enabled row (rather than silently falling back to the system default). */ private boolean agentModelOverrideResolved(AgentEntity entity, ModelConfigEntity resolved) { if (entity == null || resolved == null) return false; String agentModelName = entity.getModelName(); if (agentModelName == null || agentModelName.isBlank()) return false; return agentModelName.equalsIgnoreCase(resolved.getModelName()); } /** * 根据 AgentEntity 构建完整的 Agent 实例。 * *
{@code modelProvider} / {@code modelName} carry an optional * per-conversation model pin; when both are blank the build falls back to * the Agent's model override, then the global default.
*/ public BaseAgent build(AgentEntity entity, String modelProvider, String modelName) { AgentToolSet toolSet = toolRegistry.getEnabledToolSet(); // Move 6 — Permission flattening at build time. // // Two layers of tool filtering exist in MateClaw: // (1) Build-time filter (HERE) — decides which tools the model SEES // in the tool list. Computed once per agent build; stable for // the agent's lifecycle unless bindings change. // (2) Runtime guard (ToolGuardService.evaluate) — decides which // tools the model can CALL. Runs on every invocation; checks // workspace boundaries, sensitive paths, credential exposure, // shell command patterns, and approval workflows. All dynamic // (depends on tool arguments, not just tool name). // // The build-time filter previously ran as 4 separate passes // (deny → allow → deny → exclude). Move 6 consolidates them into // a single deny-set + a single allow-set, applied in two passes: // denied = global denied ∪ skill-discovery denied ∪ {load_skill if disabled} // allowed = agent's bound tools (null = global default) Set* The framework treats "recursion limit reached" as a normal completion — * it emits a {@code done} signal with no exception and no log. That makes * it indistinguishable from a real final answer downstream, and is the * mechanism by which a turn can silently stop mid-execution and persist * only whatever partial content the accumulator happened to hold. *
* To avoid that class of bug, the recursion limit must be sized so it can * never trip before the soft cap (ObservationDispatcher → * LimitExceededNode), which is the only path that produces a proper * {@code finish_reason} and human-facing message. Sized for the maximum * effective soft cap (DB hard ceiling + thinking-mode bonus) multiplied * by 4 (each iteration is worst-case reasoning + summarizing + action + * observation) plus a 100-step buffer for phase nodes, approval replays * and tool-result chunking. Decoupled from the per-agent value so a small * {@code max_iterations} can never accidentally re-introduce the silent * killer. *
* The base segment budget is further multiplied to cover goal-driven "hard
* continuations" — each grants a fresh full iteration budget after a
* max-iterations turn (see {@code GoalEvaluationNode}). One run can perform
* up to {@link vip.mate.goal.config.GoalProperties#MAX_HARD_CONTINUATIONS_CEILING} of them, so
* the ceiling is sized for {@code (1 + CEILING)} segments to keep the
* recursion guard from tripping before the soft caps do.
*/
private static int frameworkRecursionLimit() {
int perSegment = (BaseAgent.MAX_ITERATIONS_HARD_CEILING + 5) * 4;
return perSegment * (1 + vip.mate.goal.config.GoalProperties.MAX_HARD_CONTINUATIONS_CEILING) + 100;
}
static long resolveStreamIdleTimeoutSeconds(ModelConfigEntity modelConfig) {
Integer override = modelConfig != null
? modelConfig.getRequestTimeoutSeconds()
: null;
return HttpTimeouts.resolveStreamIdleTimeout(override).toSeconds();
}
CompiledGraph buildReActGraph(AgentToolSet toolSet, ChatModel chatModel, int maxIterations, String reasoningEffort) {
return buildReActGraph(toolSet, chatModel, maxIterations, reasoningEffort, null, null);
}
CompiledGraph buildReActGraph(AgentToolSet toolSet, ChatModel chatModel, int maxIterations,
String reasoningEffort, ModelConfigEntity primaryModelConfig) {
return buildReActGraph(toolSet, chatModel, maxIterations, reasoningEffort, primaryModelConfig, null);
}
CompiledGraph buildReActGraph(AgentToolSet toolSet, ChatModel chatModel, int maxIterations,
String reasoningEffort, ModelConfigEntity primaryModelConfig,
Long agentId) {
return buildReActGraph(toolSet, chatModel, maxIterations, reasoningEffort,
primaryModelConfig, agentId, null);
}
CompiledGraph buildReActGraph(AgentToolSet toolSet, ChatModel chatModel, int maxIterations,
String reasoningEffort, ModelConfigEntity primaryModelConfig,
Long agentId, SkillCatalogRenderer skillCatalogRenderer) {
return buildReActGraph(toolSet, chatModel, maxIterations, reasoningEffort,
primaryModelConfig, agentId, skillCatalogRenderer, null, Set.of());
}
CompiledGraph buildReActGraph(AgentToolSet toolSet, ChatModel chatModel, int maxIterations,
String reasoningEffort, ModelConfigEntity primaryModelConfig,
Long agentId, SkillCatalogRenderer skillCatalogRenderer,
PrefixBudgetPlan prefixBudgetPlan, Set
* 用于调用方(如 Wiki 消化管线)已经有自己的外层重试策略,
* 希望绕过 Spring AI 内层重试、独占重试控制权的场景:传入
* {@code RetryTemplate.builder().maxAttempts(1).build()} 即可把内层降级为"只跑一次"。
*
* DashScope 和 OpenAI-ChatGPT 分支不走 Spring AI 的 RetryTemplate 接口,
* 本参数对它们无效(它们各自有内部重试或直通)。
*/
public ChatModel buildRuntimeChatModel(ModelConfigEntity runtimeModel, RetryTemplate retryOverride) {
// PR-0 (RFC-009 Phase 4 prelude): protocol switch extracted to
// ProviderChatModelFactory + per-protocol ChatModelBuilder strategies.
// Per-protocol builders (DashScope / OpenAI-compatible / Anthropic /
// ChatGPT-Responses) live in vip.mate.agent.chatmodel + vip.mate.llm.chatmodel.
// See RFC-009 Phase 4 plan for the rationale (circular-dep break for
// ProviderInitProbe + AgentGraphBuilder slimming).
return chatModelFactory.buildFor(runtimeModel, retryOverride);
}
/**
* RFC-009: build the full multi-provider failover chain for a primary
* model. Providers are read from {@code mate_model_provider} ordered by
* {@code fallback_priority ASC} (positive values only), each resolved to
* its default {@link ModelConfigEntity} and turned into a {@link ChatModel}
* via {@link #buildRuntimeChatModel(ModelConfigEntity, RetryTemplate)}.
*
* Providers whose API key / base URL is missing (build throws) are
* silently skipped with a warning — fallback should never break
* the primary call path. The returned list preserves chain order; the
* streaming helper tries entries in order until one succeeds. The primary model is excluded from the chain when its provider +
* model name matches a chain entry. Previously only reference equality
* was checked, which meant a DashScope-primary deployment ended up with
* {@code null} fallback — exactly the case RFC-009 targets. Source = the available pool (RFC-009 follow-up). Earlier this
* method only considered providers with {@code fallback_priority > 0}, which
* meant any provider the user hadn't explicitly opted into the chain was
* silently excluded — even if it was healthy and in the pool. The pool is
* the source of truth for "what's usable right now"; {@code fallback_priority}
* is just an ordering hint within the pool. Per-provider model selection falls back gracefully: the
* provider's {@code is_default=true} chat model wins, otherwise we pick
* the first enabled chat model on that provider. Forcing users to mark a
* default per provider was administrative friction with no real benefit. Head: the agent's explicit preference entries in declared order,
* model-granular — the same provider may appear more than once with
* different models. Exact (provider, model) duplicates are dropped.
*
* Tail: every provider not named in the preferences, in the supplied
* global order, each using its default model ({@code modelId == null}).
*
* Preference entries with a blank provider id are ignored. Package-private
* for unit testing — see {@code AgentGraphBuilderPreferenceTest}.
*/
static List Precedence:
* agent-4: when any loaded skill declares structured {@code constraints}
* in its manifest, the rendered catalog gets a trailing anchor note
* {@code "🔒 = 含固定约束的 skill(详见 ProgressLedger)"} so the LLM has
* a visible cue that some skills carry non-negotiable rules pinned into
* the ledger. The cue is appended (not interleaved) to keep the
* catalog's prompt-cache hash stable for the unchanged prefix.
*/
private SkillCatalogRenderer buildSkillCatalogRenderer(AgentEntity entity, Set
*
* Returns {@code null} when the provider has no usable chat model.
*/
private ModelConfigEntity pickFallbackModel(String providerId) {
try {
ModelConfigEntity defaultModel = modelConfigService.getDefaultModelByProvider(providerId);
if (defaultModel != null) return defaultModel;
} catch (Exception ignored) {
// No default — fall through to first-enabled lookup.
}
try {
return modelConfigService.listModelsByProvider(providerId).stream()
.filter(m -> Boolean.TRUE.equals(m.getEnabled()))
.filter(m -> m.getModelType() == null || "chat".equals(m.getModelType()))
.findFirst()
.orElse(null);
} catch (Exception e) {
log.warn("[LlmFailover] cannot list models for provider {}: {}", providerId, e.getMessage());
return null;
}
}
/**
* Plan the fallback order as a list of (provider, model) refs.
*
*
*
*
* @throws IllegalArgumentException when an absolute override escapes the
* workspace root
*/
public static String resolveAgentBasePath(String agentOverride, String workspaceBase) {
boolean hasOverride = agentOverride != null && !agentOverride.isBlank();
boolean hasWorkspace = workspaceBase != null && !workspaceBase.isBlank();
if (!hasOverride) {
return hasWorkspace ? workspaceBase : null;
}
Path overridePath = Paths.get(agentOverride);
if (overridePath.isAbsolute()) {
if (hasWorkspace) {
Path wsRoot = Paths.get(workspaceBase).toAbsolutePath().normalize();
Path absOverride = overridePath.toAbsolutePath().normalize();
if (!absOverride.startsWith(wsRoot)) {
throw new IllegalArgumentException(
"Agent workspaceBasePath override must be inside the workspace root: "
+ absOverride + " is not under " + wsRoot);
}
}
return agentOverride;
}
if (hasWorkspace) {
// Relative override resolves under the workspace root; reject any value
// that escapes it via "../" so attachment/media/tool I/O stays contained.
Path wsRoot = Paths.get(workspaceBase).toAbsolutePath().normalize();
Path resolved = wsRoot.resolve(agentOverride).normalize();
if (!resolved.startsWith(wsRoot)) {
throw new IllegalArgumentException(
"Agent workspaceBasePath override must stay inside the workspace root: "
+ resolved + " escapes " + wsRoot);
}
return Paths.get(workspaceBase).resolve(agentOverride).toString();
}
return agentOverride;
}
/**
* Finds the first enabled chat model whose provider is fully configured.
* Used as a fallback when the default model's provider is not available.
*/
private ModelConfigEntity findFirstAvailableChatModel() {
return modelConfigService.listByType("chat").stream()
.filter(m -> Boolean.TRUE.equals(m.getEnabled()))
.filter(m -> {
try {
return modelProviderService.isProviderConfigured(m.getProvider());
} catch (Exception e) {
return false;
}
})
.findFirst()
.orElse(null);
}
// PR-0b: legacy single-fallback buildFallbackModel deleted (already @Deprecated, no callers).
// PR-0b: isDashScopeSearchEnabled moved to DashScopeChatModelBuilder.
// ==================== Prompt 构建 ====================
/**
* Cache-stable platform identity, appended to every agent's system
* prompt. Answers "who are you / what are you based on". The volatile
* "which model right now" fact is injected per-turn by
* {@link vip.mate.agent.context.RuntimeContextInjector} instead, to
* keep this prefix's prompt-cache hash stable.
*/
static final String ABOUT_YOU_BLOCK = """
## About You
You are powered by MateClaw — a multi-user AI Agent platform built on
Spring Boot 3.5 and Spring AI Alibaba Graph. You are reachable through
WebChat and 8+ IM channels (DingTalk, Feishu, WeCom, WeChat, Telegram,
Discord, QQ, Slack). If asked who you are or what you are based on,
answer with MateClaw and the technology stack above.
""";
private String buildEnhancedPrompt(AgentEntity entity, boolean builtinSearchEnabled) {
return buildEnhancedPrompt(entity, builtinSearchEnabled, Integer.MAX_VALUE);
}
private String buildEnhancedPrompt(AgentEntity entity, boolean builtinSearchEnabled, int memoryBudgetTokens) {
// The agent's own systemPrompt encodes its identity (role / goal /
// backstory). The memory block from workspace files (AGENTS.md, SOUL.md,
// PROFILE.md, MEMORY.md, ...) augments that identity with durable
// context. Both are independently optional, but when both exist they
// must be joined — earlier this branch picked memory and silently
// dropped the identity prompt, so editor-side identity changes never
// reached runtime if the agent had any workspace files.
String identityPrompt = entity.getSystemPrompt() != null ? entity.getSystemPrompt().trim() : "";
String memoryPrompt = memoryManager.buildSystemPromptBlock(entity.getId(), memoryBudgetTokens);
StringBuilder basePromptBuilder = new StringBuilder();
if (!identityPrompt.isEmpty()) {
basePromptBuilder.append(identityPrompt);
}
if (memoryPrompt != null && !memoryPrompt.isBlank()) {
if (basePromptBuilder.length() > 0) {
basePromptBuilder.append("\n\n");
}
basePromptBuilder.append(memoryPrompt);
}
String basePrompt = basePromptBuilder.toString();
// The skill catalog (## Skills) is NOT baked here. It is rendered at
// runtime by the reasoning / step-execution nodes via
// SkillCatalogRenderer so its ordering can react to skills loaded this
// run (load_skill pins). Keeping it out of the baked system prompt also
// keeps the prompt-cache prefix stable across turns.
// 工具调用指导
String toolGuidance = """
## Runtime Context
- Current Agent ID: %s
## Workspace Memory Guidelines
Your durable memory is stored in database-backed workspace markdown files for this agent:
- `PROFILE.md`: stable user profile, preferences, collaboration style
- `MEMORY.md`: distilled long-term memory, durable facts, lessons, recurring patterns
- `memory/YYYY-MM-DD.md`: daily notes, raw events, temporary observations, open loops
Use workspace memory tools instead of local filesystem tools for those files:
- `list_workspace_memory_files(agentId=..., filenamePrefix=...)`
- `read_workspace_memory_file(agentId=..., filename=...)`
- `write_workspace_memory_file(agentId=..., filename=..., content=...)`
- `edit_workspace_memory_file(agentId=..., filename=..., oldText=..., newText=...)`
Memory writing policy:
- Stable user preference, identity, collaboration habit -> `PROFILE.md`
- Stable project fact, workflow, tool setup, lesson learned, recurring decision -> `MEMORY.md`
- One-off event, meeting note, temporary context, today's decision trace -> `memory/YYYY-MM-DD.md`
- Read before write unless you are creating a brand new daily note
- Do not store secrets or highly sensitive data unless the user explicitly asks
- Updating workspace memory files is internal state maintenance for this agent and can be done proactively when useful
Memory emergence policy:
- If the same preference, constraint, workflow, or lesson appears repeatedly, consolidate it from daily notes into `MEMORY.md`
- Prefer updating an existing section over appending duplicate bullets
- Treat `MEMORY.md` as a compact mental model, not a raw transcript dump
- When answering tasks involving prior decisions, preferences, habits, or ongoing work, proactively consult relevant workspace memory first
## Structured Memory Tools
For discrete, typed facts use structured memory tools (separate from workspace files):
- `remember_structured(agentId, type, key, content)` — store a typed entry
- `recall_structured(agentId, type, keyword)` — search entries by type and/or keyword
- `forget_structured(agentId, type, key)` — remove an entry
Types:
- `user`: preferences, expertise, communication style, role
- `feedback`: behavioral corrections or confirmed approaches (include WHY)
- `project`: decisions, deadlines, constraints not derivable from code/git
- `reference`: pointers to external systems (Linear boards, Grafana dashboards, Slack channels)
Use workspace memory tools (MEMORY.md, daily notes) for long-form narrative notes.
Use structured memory tools for key-value facts the system can query efficiently.
## Memory vs Knowledge Base Precedence
When a question is about the user themselves — who they are, their current
project, its name/codename, tech stack, goals, metrics, budget, team, or what
they are working on — your recalled memory (the