flzxsqc-demo 1.4.26 → 1.4.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/agent-backend/docs/customer-service-excellent-case-mining/customer-service-excellent-case-mining-requirement-breakdown-v1.docx +0 -0
  2. package/agent-backend/src/main/java/com/example/agent/application/harness/tool/ToolInvocationGuard.java +42 -9
  3. package/agent-backend/src/main/java/com/example/agent/application/service/AgentTurnRunner.java +86 -4
  4. package/agent-backend/src/main/java/com/example/agent/application/service/RuntimeEventBus.java +42 -5
  5. package/agent-backend/src/main/java/com/example/agent/application/service/ToolArgumentPlannerService.java +68 -21
  6. package/agent-backend/src/main/java/com/example/agent/application/service/filter/AgentSessionFilter.java +30 -8
  7. package/agent-backend/src/main/java/com/example/agent/application/service/report/MarkdownReportDocxConverter.java +94 -38
  8. package/agent-backend/src/main/java/com/example/agent/application/service/tool/argument/SkillPerRecordAnalysisApiToolArgumentResolver.java +98 -0
  9. package/agent-backend/src/main/java/com/example/agent/domain/model/ToolPrecondition.java +5 -0
  10. package/agent-backend/src/main/java/com/example/agent/infrastructure/es/ElasticsearchLargeContentStore.java +5 -23
  11. package/agent-backend/src/main/java/com/example/agent/infrastructure/llm/DummyLlmClient.java +226 -8
  12. package/agent-backend/src/main/java/com/example/agent/infrastructure/repository/AgentEventRepository.java +2 -2
  13. package/agent-backend/src/main/java/com/example/agent/infrastructure/tool/ToolsApiToolExecutor.java +1 -0
  14. package/agent-backend/src/main/java/com/example/agent/interfaces/rest/ToolsController.java +7 -2
  15. package/agent-backend/src/main/resources/data.sql +172 -3
  16. package/agent-backend/src/test/java/com/example/agent/application/harness/tool/ToolInvocationGuardTest.java +55 -0
  17. package/agent-backend/src/test/java/com/example/agent/application/service/RuntimeEventBusTest.java +67 -0
  18. package/agent-backend/src/test/java/com/example/agent/application/service/ToolArgumentPlannerServiceTest.java +81 -0
  19. package/agent-backend/src/test/java/com/example/agent/application/service/tool/argument/SkillPerRecordAnalysisApiToolArgumentResolverTest.java +94 -0
  20. package/agent-backend/src/test/java/com/example/agent/infrastructure/repository/AgentEventRepositoryTest.java +34 -0
  21. package/package.json +1 -1
  22. package/agent-backend/docs/innovation-capability-review/assets-v8/fig01-technical-overview.png +0 -0
  23. package/agent-backend/docs/innovation-capability-review/assets-v8/fig01-technical-overview.svg +0 -171
  24. package/agent-backend/docs/innovation-capability-review/assets-v8/fig02-harness-controlled-agent.png +0 -0
  25. package/agent-backend/docs/innovation-capability-review/assets-v8/fig02-harness-controlled-agent.svg +0 -131
  26. package/agent-backend/docs/innovation-capability-review/assets-v8/fig03-harness-state-contract.png +0 -0
  27. package/agent-backend/docs/innovation-capability-review/assets-v8/fig03-harness-state-contract.svg +0 -118
  28. package/agent-backend/docs/innovation-capability-review/assets-v8/fig04-evidence-batch-analysis.png +0 -0
  29. package/agent-backend/docs/innovation-capability-review/assets-v8/fig04-evidence-batch-analysis.svg +0 -145
  30. package/agent-backend/docs/innovation-capability-review/assets-v8/fig05-analysis-discovery.png +0 -0
  31. package/agent-backend/docs/innovation-capability-review/assets-v8/fig05-analysis-discovery.svg +0 -111
  32. package/agent-backend/docs/innovation-capability-review/assets-v8/fig06-human-ai-improvement-loop.png +0 -0
  33. package/agent-backend/docs/innovation-capability-review/assets-v8/fig06-human-ai-improvement-loop.svg +0 -108
  34. package/agent-backend/docs/innovation-capability-review/assets-v8/fig07-prompt-dynamic-injection.png +0 -0
  35. package/agent-backend/docs/innovation-capability-review/assets-v8/fig07-prompt-dynamic-injection.svg +0 -129
  36. package/agent-backend/docs/innovation-capability-review/company-innovation-capability-review-v10.docx +0 -0
@@ -14,6 +14,7 @@ import com.example.agent.domain.model.ToolValidationException;
14
14
  import org.springframework.beans.factory.annotation.Autowired;
15
15
  import org.springframework.stereotype.Component;
16
16
 
17
+ import java.util.ArrayList;
17
18
  import java.util.List;
18
19
  import java.util.Map;
19
20
 
@@ -125,25 +126,57 @@ public class ToolInvocationGuard {
125
126
 
126
127
  private void validateRequiredArtifact(AgentSession session, ToolDefinition definition, ToolPrecondition precondition) {
127
128
  String artifact = trimToNull(precondition.getArtifact());
128
- String sourceTool = trimToNull(precondition.getSourceTool());
129
- if (artifact == null || sourceTool == null) {
129
+ List<String> sourceTools = sourceTools(precondition);
130
+ if (artifact == null || sourceTools.isEmpty()) {
130
131
  return;
131
132
  }
133
+ if (hasRequiredArtifact(session, sourceTools, artifact)) {
134
+ return;
135
+ }
136
+ throw validation(
137
+ "TOOL_PRECONDITION_FAILED",
138
+ "Tool '" + definition.getToolId() + "' requires artifact '" + artifact
139
+ + "' from one of tools " + sourceTools + ".",
140
+ "Call one of " + sourceTools + " first, then bind artifacts." + artifact + " before retrying."
141
+ );
142
+ }
143
+
144
+ private List<String> sourceTools(ToolPrecondition precondition) {
145
+ List<String> result = new ArrayList<String>();
146
+ addSourceTool(result, precondition.getSourceTool());
147
+ if (precondition.getSourceTools() != null) {
148
+ for (String sourceTool : precondition.getSourceTools()) {
149
+ addSourceTool(result, sourceTool);
150
+ }
151
+ }
152
+ return result;
153
+ }
154
+
155
+ private void addSourceTool(List<String> sourceTools, String sourceTool) {
156
+ String normalized = trimToNull(sourceTool);
157
+ if (normalized != null && !sourceTools.contains(normalized)) {
158
+ sourceTools.add(normalized);
159
+ }
160
+ }
161
+
162
+ private boolean hasRequiredArtifact(AgentSession session, List<String> sourceTools, String artifact) {
132
163
  for (int i = session.getExecutedSteps().size() - 1; i >= 0; i--) {
133
164
  AgentExecutedStep step = session.getExecutedSteps().get(i);
134
- if (step == null || !"SUCCEEDED".equals(step.getStatus()) || !sourceTool.equals(step.getToolName())) {
165
+ if (!isSuccessfulSource(step, sourceTools)) {
135
166
  continue;
136
167
  }
137
168
  Object value = step.getArtifacts().get(artifact);
138
169
  if (value != null && trimToNull(String.valueOf(value)) != null) {
139
- return;
170
+ return true;
140
171
  }
141
172
  }
142
- throw validation(
143
- "TOOL_PRECONDITION_FAILED",
144
- "Tool '" + definition.getToolId() + "' requires artifact '" + artifact + "' from tool '" + sourceTool + "'.",
145
- "Call '" + sourceTool + "' first, then bind artifacts." + artifact + " before retrying."
146
- );
173
+ return false;
174
+ }
175
+
176
+ private boolean isSuccessfulSource(AgentExecutedStep step, List<String> sourceTools) {
177
+ return step != null
178
+ && "SUCCEEDED".equals(step.getStatus())
179
+ && sourceTools.contains(step.getToolName());
147
180
  }
148
181
 
149
182
  private void validateRequiredFields(ToolDefinition definition, String root, Map<String, Object> source, List<String> requiredPaths) {
@@ -20,6 +20,8 @@ import com.example.agent.domain.model.chat.ChatRole;
20
20
  import com.example.agent.domain.port.LlmClient;
21
21
  import com.fasterxml.jackson.databind.ObjectMapper;
22
22
  import lombok.RequiredArgsConstructor;
23
+ import org.slf4j.Logger;
24
+ import org.slf4j.LoggerFactory;
23
25
  import org.springframework.beans.factory.annotation.Qualifier;
24
26
  import org.springframework.core.task.TaskExecutor;
25
27
  import org.springframework.stereotype.Service;
@@ -46,6 +48,7 @@ import java.util.UUID;
46
48
  */
47
49
  public class AgentTurnRunner {
48
50
  private static final int MAX_ROUNDS = 16;
51
+ private static final Logger log = LoggerFactory.getLogger(AgentTurnRunner.class);
49
52
 
50
53
  private final SkillSelector skillSelector;
51
54
  private final SkillRegistry skillRegistry;
@@ -298,13 +301,30 @@ public class AgentTurnRunner {
298
301
  planningDetails.put("round", roundNumber);
299
302
  harnessWorkflowService.started(session.getSessionId(), turn.getTurnId(), HarnessPhase.PLAN_NEXT_ACTION, planningDetails);
300
303
  ToolActionRouter.ToolActionDecision decision;
304
+ long actionRouterStartedAt = System.currentTimeMillis();
305
+ log.info("Agent action router started sessionId={} turnId={} round={} allowedToolCount={}",
306
+ session.getSessionId(), turn.getTurnId(), roundNumber, allowedTools == null ? 0 : allowedTools.size());
301
307
  try {
302
308
  decision = toolActionRouter.decide(session, skills, allowedTools, roundNumber);
303
309
  } catch (RuntimeException e) {
304
310
  // 路由模型不可用时退回旧的组合式规划器以维持可用性;错误被写入 Harness 详情,便于后续删除该兼容路径。
311
+ log.warn("Agent action router failed; combined fallback will start sessionId={} turnId={} round={} elapsedMs={} errorType={} error={}",
312
+ session.getSessionId(),
313
+ turn.getTurnId(),
314
+ roundNumber,
315
+ System.currentTimeMillis() - actionRouterStartedAt,
316
+ e.getClass().getSimpleName(),
317
+ e.getMessage());
305
318
  planningDetails.put("action_router_fallback_error", e.getMessage());
306
319
  return fallbackCombinedPlannerDecision(session, turn, allowedTools, roundNumber, planningDetails);
307
320
  }
321
+ log.info("Agent action router completed sessionId={} turnId={} round={} elapsedMs={} action={} selectedToolId={}",
322
+ session.getSessionId(),
323
+ turn.getTurnId(),
324
+ roundNumber,
325
+ System.currentTimeMillis() - actionRouterStartedAt,
326
+ decision.getActionType(),
327
+ decision.getToolId());
308
328
  planningDetails.put("selected_tool_id", decision.getToolId());
309
329
  planningDetails.put("decision_reason", decision.getReason());
310
330
  planningDetails.put("candidate_tool_ids", toolIds(allowedTools));
@@ -320,8 +340,30 @@ public class AgentTurnRunner {
320
340
  }
321
341
  ToolDefinition selectedTool = selectedTool(decision.getToolId(), allowedTools);
322
342
  // 动作路由只选择工具;参数规划仅暴露被选中工具的 Schema,避免模型混淆多个工具字段并降低上下文开销。
323
- ToolArgumentPlannerService.ArgumentPlanningResult argumentPlanningResult =
324
- toolArgumentPlannerService.planToolCall(session, skills, selectedTool, roundNumber);
343
+ long argumentPlannerStartedAt = System.currentTimeMillis();
344
+ log.info("Agent argument planner started sessionId={} turnId={} round={} selectedToolId={}",
345
+ session.getSessionId(), turn.getTurnId(), roundNumber, selectedTool.getToolId());
346
+ ToolArgumentPlannerService.ArgumentPlanningResult argumentPlanningResult;
347
+ try {
348
+ argumentPlanningResult = toolArgumentPlannerService.planToolCall(session, skills, selectedTool, roundNumber);
349
+ } catch (RuntimeException e) {
350
+ log.warn("Agent argument planner failed sessionId={} turnId={} round={} selectedToolId={} elapsedMs={} errorType={} error={}",
351
+ session.getSessionId(),
352
+ turn.getTurnId(),
353
+ roundNumber,
354
+ selectedTool.getToolId(),
355
+ System.currentTimeMillis() - argumentPlannerStartedAt,
356
+ e.getClass().getSimpleName(),
357
+ e.getMessage());
358
+ throw e;
359
+ }
360
+ log.info("Agent argument planner completed sessionId={} turnId={} round={} selectedToolId={} elapsedMs={} retryCount={}",
361
+ session.getSessionId(),
362
+ turn.getTurnId(),
363
+ roundNumber,
364
+ selectedTool.getToolId(),
365
+ System.currentTimeMillis() - argumentPlannerStartedAt,
366
+ argumentPlanningResult.getRetryCount());
325
367
  ChatMessage reply = argumentPlanningResult.getReply();
326
368
  runtimeEventBus.publish(session.getSessionId(), turn.getTurnId(), RuntimeEventType.MODEL_DECISION,
327
369
  payload("content", reply.getContent(), "toolCalls", reply.getToolCalls(), "round", roundNumber,
@@ -341,7 +383,33 @@ public class AgentTurnRunner {
341
383
  int roundNumber,
342
384
  Map<String, Object> planningDetails) {
343
385
  List<ChatMessage> messagesForLlm = contextBudgetService.prepareMessagesForToolPlanning(session, allowedTools);
344
- ChatMessage reply = LlmRoleContext.call(ModelRole.PLANNER, () -> llmClient.chat(messagesForLlm, allowedTools));
386
+ long fallbackStartedAt = System.currentTimeMillis();
387
+ log.info("Agent combined fallback started sessionId={} turnId={} round={} messageCount={} allowedToolCount={}",
388
+ session.getSessionId(),
389
+ turn.getTurnId(),
390
+ roundNumber,
391
+ messagesForLlm.size(),
392
+ allowedTools == null ? 0 : allowedTools.size());
393
+ ChatMessage reply;
394
+ try {
395
+ reply = LlmRoleContext.call(ModelRole.PLANNER, () -> llmClient.chat(messagesForLlm, allowedTools));
396
+ } catch (RuntimeException e) {
397
+ log.warn("Agent combined fallback failed sessionId={} turnId={} round={} elapsedMs={} errorType={} error={}",
398
+ session.getSessionId(),
399
+ turn.getTurnId(),
400
+ roundNumber,
401
+ System.currentTimeMillis() - fallbackStartedAt,
402
+ e.getClass().getSimpleName(),
403
+ e.getMessage());
404
+ throw e;
405
+ }
406
+ log.info("Agent combined fallback completed sessionId={} turnId={} round={} elapsedMs={} contentChars={} toolCallCount={}",
407
+ session.getSessionId(),
408
+ turn.getTurnId(),
409
+ roundNumber,
410
+ System.currentTimeMillis() - fallbackStartedAt,
411
+ reply.getContent() == null ? 0 : reply.getContent().length(),
412
+ reply.getToolCalls() == null ? 0 : reply.getToolCalls().size());
345
413
  runtimeEventBus.publish(session.getSessionId(), turn.getTurnId(), RuntimeEventType.MODEL_DECISION,
346
414
  payload("content", reply.getContent(), "toolCalls", reply.getToolCalls(), "round", roundNumber,
347
415
  "allowed_tool_ids", toolIds(allowedTools), "report_tool_allowed", containsTool(allowedTools, "build_keyword_report"),
@@ -413,13 +481,27 @@ public class AgentTurnRunner {
413
481
  }
414
482
 
415
483
  private List<SkillDefinition> questionRequirementSkills(List<SkillDefinition> selectedSkills) {
416
- if (hasEnabledQuestionRequirements(selectedSkills)) {
484
+ if (hasQuestionRequirementsConfiguration(selectedSkills)) {
417
485
  return selectedSkills;
418
486
  }
419
487
  List<SkillDefinition> allSkills = skillRegistry.allEnabled();
420
488
  return hasEnabledQuestionRequirements(allSkills) ? allSkills : selectedSkills;
421
489
  }
422
490
 
491
+ private boolean hasQuestionRequirementsConfiguration(List<SkillDefinition> skills) {
492
+ if (skills == null) {
493
+ return false;
494
+ }
495
+ for (SkillDefinition skill : skills) {
496
+ if (skill != null
497
+ && skill.getQuestionRequirements() != null
498
+ && skill.getQuestionRequirements().containsKey("enabled")) {
499
+ return true;
500
+ }
501
+ }
502
+ return false;
503
+ }
504
+
423
505
  private boolean hasEnabledQuestionRequirements(List<SkillDefinition> skills) {
424
506
  if (skills == null) {
425
507
  return false;
@@ -6,6 +6,8 @@ import com.example.agent.infrastructure.repository.AgentEventRepository;
6
6
  import com.fasterxml.jackson.core.type.TypeReference;
7
7
  import com.fasterxml.jackson.databind.ObjectMapper;
8
8
  import lombok.RequiredArgsConstructor;
9
+ import org.slf4j.Logger;
10
+ import org.slf4j.LoggerFactory;
9
11
  import org.springframework.stereotype.Service;
10
12
  import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
11
13
 
@@ -30,6 +32,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
30
32
  * 但 trace/span/turn/event 等稳定字段由本类统一写入。</p>
31
33
  */
32
34
  public class RuntimeEventBus {
35
+ private static final Logger log = LoggerFactory.getLogger(RuntimeEventBus.class);
36
+
33
37
  private final AgentEventRepository repository;
34
38
  private final ObjectMapper objectMapper;
35
39
  private final Map<String, CopyOnWriteArrayList<SseEmitter>> subscribers = new ConcurrentHashMap<String, CopyOnWriteArrayList<SseEmitter>>();
@@ -62,16 +66,49 @@ public class RuntimeEventBus {
62
66
  event.setTurnId((String) row.get("turn_id"));
63
67
  event.setEventType(RuntimeEventType.valueOf(String.valueOf(row.get("event_type"))));
64
68
  event.setCreatedAt(toInstant(row.get("created_at")));
65
- try {
66
- event.setPayload(objectMapper.readValue(String.valueOf(row.get("payload_json")), new TypeReference<Map<String, Object>>() {}));
67
- } catch (Exception e) {
68
- throw new RuntimeException("failed to parse event payload", e);
69
- }
69
+ event.setPayload(readHistoryPayload(row, event));
70
70
  events.add(event);
71
71
  }
72
72
  return events;
73
73
  }
74
74
 
75
+ private Map<String, Object> readHistoryPayload(Map<String, Object> row, RuntimeEvent event) {
76
+ Object rawPayload = row.get("payload_json");
77
+ if (rawPayload == null) {
78
+ log.warn("Runtime event payload unavailable eventId={} sessionId={} turnId={} eventType={} payloadRefId={}",
79
+ event.getId(), event.getSessionId(), event.getTurnId(), event.getEventType(), row.get("payload_ref_id"));
80
+ return unavailablePayload(row, "missing");
81
+ }
82
+ try {
83
+ Map<String, Object> payload = objectMapper.readValue(
84
+ String.valueOf(rawPayload),
85
+ new TypeReference<Map<String, Object>>() {});
86
+ return payload == null ? unavailablePayload(row, "empty") : payload;
87
+ } catch (Exception e) {
88
+ log.warn("Runtime event payload is invalid JSON eventId={} sessionId={} turnId={} eventType={} payloadRefId={} error={}",
89
+ event.getId(),
90
+ event.getSessionId(),
91
+ event.getTurnId(),
92
+ event.getEventType(),
93
+ row.get("payload_ref_id"),
94
+ e.getMessage());
95
+ return unavailablePayload(row, "invalid_json");
96
+ }
97
+ }
98
+
99
+ private Map<String, Object> unavailablePayload(Map<String, Object> row, String reason) {
100
+ Map<String, Object> payload = new HashMap<String, Object>();
101
+ payload.put("payload_unavailable", true);
102
+ payload.put("payload_error", reason);
103
+ if (row.get("payload_ref_id") != null) {
104
+ payload.put("payload_ref_id", row.get("payload_ref_id"));
105
+ }
106
+ if (row.get("payload_preview") != null) {
107
+ payload.put("payload_preview", row.get("payload_preview"));
108
+ }
109
+ return payload;
110
+ }
111
+
75
112
  public SseEmitter subscribe(String sessionId) {
76
113
  SseEmitter emitter = new SseEmitter(0L);
77
114
  subscribers.computeIfAbsent(sessionId, ignored -> new CopyOnWriteArrayList<SseEmitter>()).add(emitter);
@@ -31,6 +31,7 @@ import java.util.UUID;
31
31
  */
32
32
  public class ToolArgumentPlannerService {
33
33
  private static final int SKILL_PROMPT_LIMIT = 10000;
34
+ private static final String SKILL_PER_RECORD_ANALYSIS_TOOL_ID = "analyze_sessions_per_record_by_skill_async";
34
35
 
35
36
  private final LlmClient llmClient;
36
37
  private final ContextBudgetService contextBudgetService;
@@ -44,26 +45,26 @@ public class ToolArgumentPlannerService {
44
45
  List<ChatMessage> messages = argumentPlanningMessages(session, skills, selectedTool, roundNumber);
45
46
  List<ToolDefinition> selectedOnly = Collections.singletonList(selectedTool);
46
47
  ChatMessage reply = LlmRoleContext.call(ModelRole.PLANNER, () -> llmClient.chat(messages, selectedOnly));
47
- if (hasToolCalls(reply)) {
48
+ if (hasSelectedToolCall(reply, selectedTool)) {
48
49
  return new ArgumentPlanningResult(reply, tokenEstimateService.estimateMessages(messages), 0, false);
49
50
  }
50
- if (!looksLikeTextToolCall(reply == null ? null : reply.getContent())) {
51
- return new ArgumentPlanningResult(reply, tokenEstimateService.estimateMessages(messages), 0, false);
52
- }
53
- messages.add(message(ChatRole.ASSISTANT, reply.getContent()));
54
- messages.add(message(ChatRole.SYSTEM, malformedToolCallRecoveryInstruction(selectedTool)));
51
+ boolean malformedTextToolCall = looksLikeTextToolCall(reply == null ? null : reply.getContent());
52
+ addRetryContext(messages, reply, selectedTool, malformedTextToolCall);
55
53
  ChatMessage retryReply = LlmRoleContext.call(ModelRole.PLANNER, () -> llmClient.chat(messages, selectedOnly));
56
- if (hasToolCalls(retryReply)) {
57
- return new ArgumentPlanningResult(retryReply, tokenEstimateService.estimateMessages(messages), 1, true);
54
+ if (hasSelectedToolCall(retryReply, selectedTool)) {
55
+ return new ArgumentPlanningResult(
56
+ retryReply,
57
+ tokenEstimateService.estimateMessages(messages),
58
+ 1,
59
+ malformedTextToolCall
60
+ );
58
61
  }
59
- ChatMessage repaired = repairTextToolCall(retryReply == null ? null : retryReply.getContent(), selectedTool);
60
- if (!hasToolCalls(repaired)) {
61
- repaired = repairTextToolCall(reply.getContent(), selectedTool);
62
- }
63
- if (hasToolCalls(repaired)) {
62
+ ChatMessage repaired = repairTextToolCallFromReplies(reply, retryReply, selectedTool);
63
+ if (hasSelectedToolCall(repaired, selectedTool)) {
64
64
  return new ArgumentPlanningResult(repaired, tokenEstimateService.estimateMessages(messages), 1, true);
65
65
  }
66
- return new ArgumentPlanningResult(retryReply, tokenEstimateService.estimateMessages(messages), 1, true);
66
+ throw new IllegalStateException("selected tool `" + selectedTool.getToolId()
67
+ + "` produced no executable function/tool call after one correction");
67
68
  }
68
69
 
69
70
  private List<ChatMessage> argumentPlanningMessages(AgentSession session,
@@ -88,11 +89,28 @@ public class ToolArgumentPlannerService {
88
89
  builder.append("Use the selected tool schema supplied by the API. Do not call any other tool.\n");
89
90
  builder.append("Use source_ref/source_refs for prior evidence when possible. Do not copy large prior tool outputs into arguments.\n");
90
91
  builder.append("Relevant skill details:\n");
92
+ appendRelevantSkillDetails(builder, skills, selectedTool);
93
+ return builder.toString();
94
+ }
95
+
96
+ private void appendRelevantSkillDetails(StringBuilder builder,
97
+ List<SkillDefinition> skills,
98
+ ToolDefinition selectedTool) {
99
+ if (isSkillPerRecordAnalysisTool(selectedTool)) {
100
+ builder.append("### server-managed-skill-analysis\n");
101
+ builder.append("The runtime injects the excellent-case analysis prompt and fixed analysis_targets. ");
102
+ builder.append("Do not generate analysis_prompt or analysis_targets. ");
103
+ builder.append("Generate the queryId from the prior query handle.\n\n");
104
+ return;
105
+ }
91
106
  for (SkillDefinition skill : relevantSkills(skills, selectedTool.getToolId())) {
92
107
  builder.append("### ").append(skill.getSkillId()).append("\n");
93
108
  builder.append(truncate(skill.getPromptMarkdown(), SKILL_PROMPT_LIMIT)).append("\n\n");
94
109
  }
95
- return builder.toString();
110
+ }
111
+
112
+ private boolean isSkillPerRecordAnalysisTool(ToolDefinition selectedTool) {
113
+ return selectedTool != null && SKILL_PER_RECORD_ANALYSIS_TOOL_ID.equals(selectedTool.getToolId());
96
114
  }
97
115
 
98
116
  private List<SkillDefinition> relevantSkills(List<SkillDefinition> skills, String toolId) {
@@ -108,10 +126,33 @@ public class ToolArgumentPlannerService {
108
126
  return result.isEmpty() ? skills : result;
109
127
  }
110
128
 
111
- private String malformedToolCallRecoveryInstruction(ToolDefinition selectedTool) {
112
- return "Runtime correction: the previous response wrote a tool call as assistant text. "
113
- + "That is invalid. Call `" + selectedTool.getToolId()
114
- + "` using the actual function/tool channel now. Do not include prose or raw tool_call JSON.";
129
+ private void addRetryContext(List<ChatMessage> messages,
130
+ ChatMessage reply,
131
+ ToolDefinition selectedTool,
132
+ boolean malformedTextToolCall) {
133
+ if (reply != null && hasText(reply.getContent())) {
134
+ messages.add(message(ChatRole.ASSISTANT, reply.getContent()));
135
+ }
136
+ messages.add(message(ChatRole.SYSTEM, toolCallRecoveryInstruction(selectedTool, malformedTextToolCall)));
137
+ }
138
+
139
+ private String toolCallRecoveryInstruction(ToolDefinition selectedTool, boolean malformedTextToolCall) {
140
+ String reason = malformedTextToolCall
141
+ ? "the previous response wrote a tool call as assistant text"
142
+ : "the previous response did not emit an executable tool call";
143
+ return "Runtime correction: " + reason + ". "
144
+ + "Call `" + selectedTool.getToolId()
145
+ + "` using the actual function/tool channel now. Emit exactly one call and no prose or raw tool_call JSON.";
146
+ }
147
+
148
+ private ChatMessage repairTextToolCallFromReplies(ChatMessage firstReply,
149
+ ChatMessage retryReply,
150
+ ToolDefinition selectedTool) {
151
+ ChatMessage repaired = repairTextToolCall(retryReply == null ? null : retryReply.getContent(), selectedTool);
152
+ if (hasSelectedToolCall(repaired, selectedTool)) {
153
+ return repaired;
154
+ }
155
+ return repairTextToolCall(firstReply == null ? null : firstReply.getContent(), selectedTool);
115
156
  }
116
157
 
117
158
  private ChatMessage repairTextToolCall(String content, ToolDefinition selectedTool) {
@@ -171,8 +212,14 @@ public class ToolArgumentPlannerService {
171
212
  && payload.containsKey("arguments");
172
213
  }
173
214
 
174
- private boolean hasToolCalls(ChatMessage reply) {
175
- return reply != null && reply.getToolCalls() != null && !reply.getToolCalls().isEmpty();
215
+ private boolean hasSelectedToolCall(ChatMessage reply, ToolDefinition selectedTool) {
216
+ if (reply == null || reply.getToolCalls() == null || reply.getToolCalls().size() != 1) {
217
+ return false;
218
+ }
219
+ ToolCall toolCall = reply.getToolCalls().get(0);
220
+ return toolCall != null
221
+ && toolCall.getFunction() != null
222
+ && selectedTool.getToolId().equals(toolCall.getFunction().getName());
176
223
  }
177
224
 
178
225
  private ChatMessage message(ChatRole role, String content) {
@@ -34,21 +34,43 @@ public class AgentSessionFilter {
34
34
  private Map<String, Object> extensions = new LinkedHashMap<String, Object>();
35
35
 
36
36
  public boolean isEmpty() {
37
+ return isBasicScopeEmpty()
38
+ && isRoutingScopeEmpty()
39
+ && isBusinessScopeEmpty()
40
+ && isResultControlEmpty();
41
+ }
42
+
43
+ private boolean isBasicScopeEmpty() {
37
44
  return sessionIds.isEmpty()
38
45
  && channel == null
39
46
  && batchCode == null
40
- && startTime == null
41
- && endTime == null
42
- && agentId == null
47
+ && isTimeRangeEmpty();
48
+ }
49
+
50
+ private boolean isTimeRangeEmpty() {
51
+ return startTime == null && endTime == null;
52
+ }
53
+
54
+ private boolean isRoutingScopeEmpty() {
55
+ return agentId == null
43
56
  && queueId == null
44
57
  && teamIds.isEmpty()
45
- && groupIds.isEmpty()
46
- && fullJourney == null
58
+ && groupIds.isEmpty();
59
+ }
60
+
61
+ private boolean isBusinessScopeEmpty() {
62
+ return fullJourney == null
47
63
  && dataSource == null
48
64
  && conversationType == null
49
- && businessLine == null
50
- && customerId == null
51
- && keywords.isEmpty()
65
+ && isBusinessIdentityEmpty();
66
+ }
67
+
68
+ private boolean isBusinessIdentityEmpty() {
69
+ return businessLine == null && customerId == null;
70
+ }
71
+
72
+ private boolean isResultControlEmpty() {
73
+ return keywords.isEmpty()
52
74
  && limit == null
53
75
  && extensions.isEmpty();
54
76
  }