diff --git a/mateclaw-server/src/main/java/vip/mate/agent/context/RuntimeContextInjector.java b/mateclaw-server/src/main/java/vip/mate/agent/context/RuntimeContextInjector.java index 7b3649b1..4f595be6 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/context/RuntimeContextInjector.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/context/RuntimeContextInjector.java @@ -67,6 +67,24 @@ public final class RuntimeContextInjector { public static String buildContextMessage(String workspaceBasePath, vip.mate.i18n.I18nService i18n, ChatOrigin origin) { + return buildContextMessage(workspaceBasePath, i18n, origin, null, null); + } + + /** + * Full overload that also renders the agent's runtime model identity. + * The model line is emitted for EVERY origin (web / cron / IM / null) + * because it describes the agent, not the caller — only the sender + * block stays IM-only. {@code modelName}/{@code providerId} come from + * graph state ({@code RUNTIME_MODEL_NAME}/{@code RUNTIME_PROVIDER_ID}), + * i.e. the model selected at run start (mid-run failover is not + * reflected — accepted trade-off). Stays well under the 1024-char + * spring-ai user-cache threshold. + */ + public static String buildContextMessage(String workspaceBasePath, + vip.mate.i18n.I18nService i18n, + ChatOrigin origin, + String modelName, + String providerId) { LocalDateTime now = LocalDateTime.now(ZONE); String dateStr = now.format(DATE_FMT); String timeStr = now.format(TIME_FMT); @@ -91,6 +109,7 @@ public final class RuntimeContextInjector { } appendSenderBlockIfPresent(sb, origin); + appendModelLineIfPresent(sb, modelName, providerId, i18n); return sb.toString(); } @@ -121,6 +140,33 @@ public final class RuntimeContextInjector { } } + /** + * Append the agent's runtime model identity. Emitted for all origins + * (it's an agent fact, not a sender fact). Skipped when modelName is + * blank. Provider parenthetical is omitted when providerId is blank. + */ + private static void appendModelLineIfPresent(StringBuilder sb, String modelName, + String providerId, + vip.mate.i18n.I18nService i18n) { + if (modelName == null || modelName.isBlank()) return; + String model = modelName.trim(); + sb.append("\n"); + if (i18n != null) { + sb.append(i18n.msg("context.model_identity", model)); + } else { + sb.append("[system-context] Model: ").append(model); + } + if (providerId != null && !providerId.isBlank()) { + sb.append(" (provider: ").append(providerId.trim()).append(')'); + } + sb.append("\n"); + if (i18n != null) { + sb.append(i18n.msg("context.model_identity_hint")); + } else { + sb.append("If asked which model you are using, answer with this value for the current run."); + } + } + /** * Append a sender / channel / chat block when the origin carries * meaningful IM context. Format is intentionally one line per diff --git a/mateclaw-server/src/main/resources/messages.properties b/mateclaw-server/src/main/resources/messages.properties index db083456..76147844 100644 --- a/mateclaw-server/src/main/resources/messages.properties +++ b/mateclaw-server/src/main/resources/messages.properties @@ -294,6 +294,8 @@ context.current_time=[system-context] \u5f53\u524d\u65f6\u95f4: {0} {1} (Asia/Sh context.working_dir=[system-context] \u5de5\u4f5c\u76ee\u5f55: {0} context.working_dir_hint=\u4f60\u53ea\u80fd\u5728\u6b64\u76ee\u5f55\u53ca\u5176\u5b50\u76ee\u5f55\u5185\u8bfb\u5199\u6587\u4ef6\u548c\u6267\u884c\u547d\u4ee4\u3002 context.skill_dir_hint=\u5171\u4eab\u6280\u80fd\u4f4d\u4e8e {0}\uff0c\u4f60\u4e5f\u53ef\u4ee5\u8bfb\u53d6\u548c\u8fd0\u884c\u5176\u4e2d\u7684\u6587\u4ef6\uff08\u5373\u4f7f\u5728\u5de5\u4f5c\u76ee\u5f55\u4e4b\u5916\uff09\u3002 +context.model_identity=[system-context] \u6a21\u578b: {0} +context.model_identity_hint=\u88ab\u95ee\u5230\u4f60\u7528\u7684\u4ec0\u4e48\u6a21\u578b\u65f6\uff0c\u6309\u672c\u8f6e\u8fd9\u4e2a\u5024\u56de\u7b54\u3002 # --- Wiki Research Fallback (RFC: prompt-cleanup) --- research.fallback.no_plan=\u65e0\u6cd5\u4e3a\u8be5\u4e3b\u9898\u751f\u6210\u7814\u7a76\u8ba1\u5212\u3002 diff --git a/mateclaw-server/src/main/resources/messages_en.properties b/mateclaw-server/src/main/resources/messages_en.properties index 823dd2f9..ee08f9de 100644 --- a/mateclaw-server/src/main/resources/messages_en.properties +++ b/mateclaw-server/src/main/resources/messages_en.properties @@ -301,6 +301,8 @@ context.current_time=[system-context] Current time: {0} {1} (Asia/Shanghai) context.working_dir=[system-context] Working directory: {0} context.working_dir_hint=You can only read/write files and execute commands within this directory and its subdirectories. context.skill_dir_hint=Shared skills live under {0}; you may also read and run files there, even though it is outside the working directory. +context.model_identity=[system-context] Model: {0} +context.model_identity_hint=If asked which model you are using, answer with this value for the current run. # --- Wiki Research Fallback (RFC: prompt-cleanup) --- research.fallback.no_plan=Unable to generate a research plan for this topic. diff --git a/mateclaw-server/src/test/java/vip/mate/agent/context/RuntimeContextInjectorModelTest.java b/mateclaw-server/src/test/java/vip/mate/agent/context/RuntimeContextInjectorModelTest.java new file mode 100644 index 00000000..0ce8012f --- /dev/null +++ b/mateclaw-server/src/test/java/vip/mate/agent/context/RuntimeContextInjectorModelTest.java @@ -0,0 +1,76 @@ +package vip.mate.agent.context; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Pin the runtime model-identity line added by the 5-arg + * {@link RuntimeContextInjector#buildContextMessage} overload. + * + *
Unlike the sender block, the model line is about the AGENT (which + * model is driving this run), not the caller — so it must appear for + * every origin, including web and cron. The legacy 3/4-arg overloads + * must stay model-free so existing eval baselines don't shift. + */ +class RuntimeContextInjectorModelTest { + + @Test + @DisplayName("model + provider present → emits Model line with provider parenthetical + hint") + void modelLineWithProvider() { + String ctx = RuntimeContextInjector.buildContextMessage( + "/data/ws/5", null, ChatOrigin.EMPTY, "gpt-4o", "openai"); + + assertTrue(ctx.contains("[system-context] Model: gpt-4o"), "model line missing: " + ctx); + assertTrue(ctx.contains("(provider: openai)"), "provider missing: " + ctx); + assertTrue(ctx.contains("answer with this value for the current run"), + "model-identity hint missing: " + ctx); + } + + @Test + @DisplayName("blank provider → Model line without provider parenthetical") + void modelLineWithoutProvider() { + String ctx = RuntimeContextInjector.buildContextMessage( + "/data/ws/5", null, ChatOrigin.EMPTY, "claude-sonnet-4-6", " "); + + assertTrue(ctx.contains("[system-context] Model: claude-sonnet-4-6"), "model line missing: " + ctx); + assertFalse(ctx.contains("(provider:"), "blank provider must not emit parenthetical: " + ctx); + } + + @Test + @DisplayName("web origin still gets the model line (agent fact, not sender fact)") + void webOriginStillGetsModelLine() { + ChatOrigin origin = ChatOrigin.web("conv_1", "user-1", 5L, "/data/ws/5"); + + String ctx = RuntimeContextInjector.buildContextMessage( + "/data/ws/5", null, origin, "gpt-4o", "openai"); + + assertFalse(ctx.contains("Channel:"), "web origin must still suppress sender block: " + ctx); + assertTrue(ctx.contains("Model: gpt-4o"), "web origin must still get model line: " + ctx); + } + + @Test + @DisplayName("blank modelName → no model line at all") + void blankModelNoLine() { + String ctx = RuntimeContextInjector.buildContextMessage( + "/data/ws/5", null, ChatOrigin.EMPTY, " ", "openai"); + + assertFalse(ctx.contains("Model:"), "blank model must not emit a model line: " + ctx); + } + + @Test + @DisplayName("IM origin → both sender block AND model line present") + void imOriginHasSenderAndModel() { + ChatOrigin origin = new ChatOrigin( + 7L, "feishu:oc_abc", "ou_xyz", 5L, "/data/ws/5", + 9L, null, false, "Alice", "feishu", "oc_abc", null); + + String ctx = RuntimeContextInjector.buildContextMessage( + "/data/ws/5", null, origin, "gpt-4o", "openai"); + + assertTrue(ctx.contains("Channel: feishu"), "sender block missing: " + ctx); + assertTrue(ctx.contains("Model: gpt-4o"), "model line missing: " + ctx); + } +}