@jsonstudio/llms 0.6.1739 → 0.6.1890

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 (107) hide show
  1. package/dist/conversion/compat/actions/deepseek-web-request.d.ts +3 -0
  2. package/dist/conversion/compat/actions/deepseek-web-request.js +350 -0
  3. package/dist/conversion/compat/actions/deepseek-web-response.d.ts +3 -0
  4. package/dist/conversion/compat/actions/deepseek-web-response.js +886 -0
  5. package/dist/conversion/compat/actions/gemini-cli-request.js +3 -1
  6. package/dist/conversion/compat/profiles/chat-deepseek-web.json +18 -0
  7. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +166 -2
  8. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +169 -0
  9. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +6 -0
  10. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +12 -0
  11. package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +1 -0
  12. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +4 -0
  13. package/dist/conversion/hub/pipeline/hub-pipeline.js +365 -144
  14. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +9 -0
  15. package/dist/conversion/hub/policy/policy-engine.d.ts +2 -0
  16. package/dist/conversion/hub/policy/policy-engine.js +8 -0
  17. package/dist/conversion/hub/process/chat-process.js +466 -16
  18. package/dist/conversion/hub/response/provider-response.js +0 -35
  19. package/dist/conversion/responses/responses-openai-bridge.d.ts +2 -0
  20. package/dist/conversion/responses/responses-openai-bridge.js +166 -8
  21. package/dist/conversion/shared/anthropic-message-utils.js +10 -1
  22. package/dist/conversion/shared/protocol-field-allowlists.d.ts +2 -2
  23. package/dist/conversion/shared/protocol-field-allowlists.js +4 -0
  24. package/dist/conversion/shared/tool-governor.js +102 -0
  25. package/dist/guidance/index.js +17 -0
  26. package/dist/router/virtual-router/bootstrap.js +46 -1
  27. package/dist/router/virtual-router/classifier.js +59 -4
  28. package/dist/router/virtual-router/engine/health/index.js +6 -6
  29. package/dist/router/virtual-router/engine/routing-state/store.js +16 -3
  30. package/dist/router/virtual-router/engine-logging.js +62 -24
  31. package/dist/router/virtual-router/engine-selection/route-utils.js +20 -20
  32. package/dist/router/virtual-router/engine-selection/tier-selection.js +2 -2
  33. package/dist/router/virtual-router/engine.d.ts +3 -1
  34. package/dist/router/virtual-router/engine.js +359 -39
  35. package/dist/router/virtual-router/features.js +2 -1
  36. package/dist/router/virtual-router/pre-command-file-resolver.d.ts +2 -0
  37. package/dist/router/virtual-router/pre-command-file-resolver.js +90 -0
  38. package/dist/router/virtual-router/provider-registry.js +3 -1
  39. package/dist/router/virtual-router/routing-instructions.d.ts +15 -1
  40. package/dist/router/virtual-router/routing-instructions.js +110 -151
  41. package/dist/router/virtual-router/routing-pre-command-actions.d.ts +3 -0
  42. package/dist/router/virtual-router/routing-pre-command-actions.js +26 -0
  43. package/dist/router/virtual-router/routing-pre-command-parser.d.ts +2 -0
  44. package/dist/router/virtual-router/routing-pre-command-parser.js +85 -0
  45. package/dist/router/virtual-router/routing-pre-command-state-codec.d.ts +3 -0
  46. package/dist/router/virtual-router/routing-pre-command-state-codec.js +24 -0
  47. package/dist/router/virtual-router/routing-stop-message-actions.d.ts +2 -0
  48. package/dist/router/virtual-router/routing-stop-message-actions.js +96 -0
  49. package/dist/router/virtual-router/routing-stop-message-parser.d.ts +3 -0
  50. package/dist/router/virtual-router/routing-stop-message-parser.js +142 -0
  51. package/dist/router/virtual-router/routing-stop-message-state-codec.d.ts +4 -0
  52. package/dist/router/virtual-router/routing-stop-message-state-codec.js +85 -0
  53. package/dist/router/virtual-router/sticky-session-store.js +206 -57
  54. package/dist/router/virtual-router/stop-message-stage-template-files.d.ts +12 -0
  55. package/dist/router/virtual-router/stop-message-stage-template-files.js +67 -0
  56. package/dist/router/virtual-router/stop-message-state-sync.d.ts +1 -1
  57. package/dist/router/virtual-router/stop-message-state-sync.js +5 -0
  58. package/dist/router/virtual-router/token-file-scanner.d.ts +9 -0
  59. package/dist/router/virtual-router/token-file-scanner.js +64 -3
  60. package/dist/router/virtual-router/tool-signals.d.ts +5 -0
  61. package/dist/router/virtual-router/tool-signals.js +42 -3
  62. package/dist/router/virtual-router/types.d.ts +19 -1
  63. package/dist/router/virtual-router/types.js +1 -0
  64. package/dist/servertool/clock/config.d.ts +1 -1
  65. package/dist/servertool/clock/config.js +27 -4
  66. package/dist/servertool/clock/state.js +41 -2
  67. package/dist/servertool/clock/task-store.d.ts +2 -2
  68. package/dist/servertool/clock/task-store.js +1 -1
  69. package/dist/servertool/clock/tasks.d.ts +3 -1
  70. package/dist/servertool/clock/tasks.js +209 -18
  71. package/dist/servertool/clock/types.d.ts +17 -0
  72. package/dist/servertool/continue-execution/log.d.ts +3 -0
  73. package/dist/servertool/continue-execution/log.js +13 -0
  74. package/dist/servertool/engine.js +414 -68
  75. package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.js +6 -6
  76. package/dist/servertool/handlers/clock-auto.js +54 -71
  77. package/dist/servertool/handlers/clock.js +121 -6
  78. package/dist/servertool/handlers/continue-execution.d.ts +1 -0
  79. package/dist/servertool/handlers/continue-execution.js +91 -0
  80. package/dist/servertool/handlers/followup-request-builder.js +13 -0
  81. package/dist/servertool/handlers/gemini-empty-reply-continue.js +1 -1
  82. package/dist/servertool/handlers/iflow-model-error-retry.js +1 -1
  83. package/dist/servertool/handlers/recursive-detection-guard.js +1 -1
  84. package/dist/servertool/handlers/stop-message-auto.js +386 -257
  85. package/dist/servertool/handlers/stop-message-stage-policy.d.ts +43 -0
  86. package/dist/servertool/handlers/stop-message-stage-policy.js +684 -0
  87. package/dist/servertool/handlers/vision.js +1 -1
  88. package/dist/servertool/log/progress-file.d.ts +14 -0
  89. package/dist/servertool/log/progress-file.js +88 -0
  90. package/dist/servertool/pre-command-hooks.d.ts +17 -0
  91. package/dist/servertool/pre-command-hooks.js +491 -0
  92. package/dist/servertool/registry.d.ts +23 -6
  93. package/dist/servertool/registry.js +66 -1
  94. package/dist/servertool/server-side-tools.d.ts +1 -0
  95. package/dist/servertool/server-side-tools.js +216 -14
  96. package/dist/servertool/stop-gateway-context.d.ts +14 -0
  97. package/dist/servertool/stop-gateway-context.js +167 -0
  98. package/dist/servertool/stop-message-compare-context.d.ts +24 -0
  99. package/dist/servertool/stop-message-compare-context.js +133 -0
  100. package/dist/servertool/types.d.ts +12 -0
  101. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +1 -0
  102. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.js +36 -1
  103. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +3 -0
  104. package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +3 -0
  105. package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +118 -1
  106. package/dist/tools/apply-patch/args-normalizer/default-actions.js +1 -1
  107. package/package.json +1 -1
@@ -19,6 +19,15 @@ export function runRespOutboundStage1ClientRemap(options) {
19
19
  requestId: options.requestId,
20
20
  ...(toolsRaw ? { toolsRaw } : {})
21
21
  });
22
+ if (options.payload && typeof options.payload === 'object' && !Array.isArray(options.payload)) {
23
+ const source = options.payload;
24
+ const passthrough = ['metadata', 'temperature', 'top_p', 'prompt_cache_key', 'reasoning'];
25
+ for (const key of passthrough) {
26
+ if (clientPayload[key] === undefined && source[key] !== undefined) {
27
+ clientPayload[key] = jsonClone(source[key]);
28
+ }
29
+ }
30
+ }
22
31
  }
23
32
  // Preserve upstream error envelope when present (used by host status mapping + servertool auto flows).
24
33
  try {
@@ -33,6 +33,7 @@ export declare function recordHubPolicyObservation(options: {
33
33
  policy?: HubPolicyConfig;
34
34
  phase?: PolicyObservation['phase'];
35
35
  providerProtocol: string;
36
+ compatibilityProfile?: string;
36
37
  payload: JsonObject;
37
38
  stageRecorder?: StageRecorder;
38
39
  requestId?: string;
@@ -40,6 +41,7 @@ export declare function recordHubPolicyObservation(options: {
40
41
  export declare function applyHubProviderOutboundPolicy(options: {
41
42
  policy?: HubPolicyConfig;
42
43
  providerProtocol: string;
44
+ compatibilityProfile?: string;
43
45
  payload: JsonObject;
44
46
  stageRecorder?: StageRecorder;
45
47
  requestId?: string;
@@ -207,6 +207,10 @@ export function recordHubPolicyObservation(options) {
207
207
  if (mode !== 'observe' && mode !== 'enforce') {
208
208
  return;
209
209
  }
210
+ const compatibilityProfile = typeof options.compatibilityProfile === 'string' ? options.compatibilityProfile.trim().toLowerCase() : '';
211
+ if (compatibilityProfile === 'chat:deepseek-web') {
212
+ return;
213
+ }
210
214
  if (!shouldSample(effectivePolicy?.sampleRate)) {
211
215
  return;
212
216
  }
@@ -236,6 +240,10 @@ export function applyHubProviderOutboundPolicy(options) {
236
240
  if (mode !== 'enforce') {
237
241
  return options.payload;
238
242
  }
243
+ const compatibilityProfile = typeof options.compatibilityProfile === 'string' ? options.compatibilityProfile.trim().toLowerCase() : '';
244
+ if (compatibilityProfile === 'chat:deepseek-web') {
245
+ return options.payload;
246
+ }
239
247
  const result = applyProviderOutboundPolicy(options.providerProtocol, options.payload);
240
248
  if (!result.changed) {
241
249
  return options.payload;
@@ -4,13 +4,23 @@ import { readRuntimeMetadata } from '../../shared/runtime-metadata.js';
4
4
  import { ensureApplyPatchSchema } from '../../shared/tool-mapping.js';
5
5
  import { normalizeApplyPatchToolCallsOnRequest } from '../../shared/tool-governor.js';
6
6
  import { buildAnthropicToolAliasMap } from '../../shared/anthropic-message-utils.js';
7
- import { clearClockSession, resolveClockConfig, reserveDueTasksForRequest, startClockDaemonIfNeeded } from '../../../servertool/clock/task-store.js';
7
+ import { clearClockSession, parseDueAtMs, resolveClockConfig, reserveDueTasksForRequest, scheduleClockTasks, startClockDaemonIfNeeded } from '../../../servertool/clock/task-store.js';
8
8
  import { logClock } from '../../../servertool/clock/log.js';
9
+ import { logContinueExecution } from '../../../servertool/continue-execution/log.js';
9
10
  import { buildTimeTagLine, getClockTimeSnapshot } from '../../../servertool/clock/ntp.js';
10
11
  import { clearPendingServerToolInjection, loadPendingServerToolInjection } from '../../../servertool/pending-session.js';
12
+ import { loadRoutingInstructionStateSync } from '../../../router/virtual-router/sticky-session-store.js';
11
13
  import { isJsonObject } from '../types/json.js';
12
14
  import { applyHubOperations } from '../ops/operations.js';
13
15
  const toolGovernanceEngine = new ToolGovernanceEngine();
16
+ const CONTINUE_EXECUTION_SYSTEM_DIRECTIVE_MARKER = '[routecodex:continue_execution_directive]';
17
+ const CONTINUE_EXECUTION_SYSTEM_DIRECTIVE = [
18
+ CONTINUE_EXECUTION_SYSTEM_DIRECTIVE_MARKER,
19
+ 'Execution policy (must follow):',
20
+ '- Do not output status-talk placeholders such as \"继续执行中\" / \"I will continue\" before real work.',
21
+ '- Perform at least one real action first (prefer tool calls), then report concise results when needed.',
22
+ '- If you are about to return finish_reason=stop (or protocol-equivalent end_turn), do not end directly; emit a continue_execution tool call first.'
23
+ ].join('\n');
14
24
  export async function runHubChatProcess(options) {
15
25
  const startTime = Date.now();
16
26
  try {
@@ -144,6 +154,9 @@ async function applyRequestToolGovernance(request, context) {
144
154
  merged = applyHubOperations(merged, buildWebSearchOperations(merged, metadata));
145
155
  // Server-side clock tool + scheduled reminders injection (config-driven, best-effort).
146
156
  merged = applyHubOperations(merged, buildClockOperations(metadata));
157
+ // Server-side no-op continue tool injection (always-on, lightweight).
158
+ merged = applyHubOperations(merged, buildContinueExecutionOperations(metadata));
159
+ merged = injectContinueExecutionDirectiveIntoUserMessage(merged, metadata);
147
160
  merged = await maybeInjectClockRemindersAndApplyDirectives(merged, metadata, context.requestId);
148
161
  const { request: sanitized, summary } = toolGovernanceEngine.governRequest(merged, providerProtocol);
149
162
  if (summary.applied) {
@@ -646,7 +659,8 @@ function buildWebSearchOperations(request, metadata) {
646
659
  function buildClockOperations(metadata) {
647
660
  const rt = readRuntimeMetadata(metadata);
648
661
  // Do not inject clock tool into internal servertool followup hops.
649
- if (rt?.serverToolFollowup === true) {
662
+ const allowClockFollowupToolInjection = rt?.clockFollowupInjectTool === true;
663
+ if (rt?.serverToolFollowup === true && !allowClockFollowupToolInjection) {
650
664
  return [];
651
665
  }
652
666
  const rawConfig = rt?.clock;
@@ -662,12 +676,12 @@ function buildClockOperations(metadata) {
662
676
  properties: {
663
677
  action: {
664
678
  type: 'string',
665
- enum: ['get', 'schedule', 'list', 'cancel', 'clear'],
666
- description: 'Get current time, or schedule/list/cancel/clear session-scoped reminders.'
679
+ enum: ['get', 'schedule', 'update', 'list', 'cancel', 'clear'],
680
+ description: 'Get current time, or schedule/update/list/cancel/clear session-scoped reminders. For any wait/later requirement, schedule immediately.'
667
681
  },
668
682
  items: {
669
683
  type: 'array',
670
- description: 'For schedule: list of reminders to add.',
684
+ description: 'For schedule/update: list of reminder payloads. update uses items[0] as patch source.',
671
685
  items: {
672
686
  type: 'object',
673
687
  properties: {
@@ -677,7 +691,7 @@ function buildClockOperations(metadata) {
677
691
  },
678
692
  task: {
679
693
  type: 'string',
680
- description: 'Reminder text (should include which tool to use and what to do).'
694
+ description: 'Reminder text that states the exact action to execute on wake-up (no vague placeholders).'
681
695
  },
682
696
  tool: {
683
697
  type: 'string',
@@ -696,7 +710,7 @@ function buildClockOperations(metadata) {
696
710
  },
697
711
  taskId: {
698
712
  type: 'string',
699
- description: 'For cancel: taskId to remove.'
713
+ description: 'For cancel/update: target taskId.'
700
714
  }
701
715
  },
702
716
  // For strict tool schemas (OpenAI Responses), required must include every key in properties.
@@ -708,7 +722,7 @@ function buildClockOperations(metadata) {
708
722
  type: 'function',
709
723
  function: {
710
724
  name: 'clock',
711
- description: 'Time + Alarm for this session. Use get/schedule/list/cancel/clear. Scheduled reminders will be injected into future requests.',
725
+ description: 'Time + Alarm for this session. MUST use clock.schedule whenever waiting/delay is required. Never promise to wait without scheduling. Use get/schedule/update/list/cancel/clear. Scheduled reminders are injected into future requests.',
712
726
  parameters,
713
727
  strict: true
714
728
  }
@@ -721,6 +735,175 @@ function buildClockOperations(metadata) {
721
735
  { op: 'append_tool_if_missing', toolName: 'clock', tool: clockTool }
722
736
  ];
723
737
  }
738
+ function hasActiveStopMessageStateForContinueExecution(metadata) {
739
+ const rt = readRuntimeMetadata(metadata);
740
+ if (isStopMessageStateActive(rt?.stopMessageState)) {
741
+ return true;
742
+ }
743
+ return hasPersistedActiveStopMessageState(metadata);
744
+ }
745
+ function hasPersistedActiveStopMessageState(metadata) {
746
+ const sessionScope = resolveStopMessageSessionScope(metadata);
747
+ if (!sessionScope) {
748
+ return false;
749
+ }
750
+ try {
751
+ const persisted = loadRoutingInstructionStateSync(sessionScope);
752
+ return isStopMessageStateActive(persisted);
753
+ }
754
+ catch {
755
+ return false;
756
+ }
757
+ }
758
+ function resolveStopMessageSessionScope(metadata) {
759
+ const sessionId = readString(metadata.sessionId);
760
+ if (sessionId) {
761
+ return 'session:' + sessionId;
762
+ }
763
+ const conversationId = readString(metadata.conversationId);
764
+ if (conversationId) {
765
+ return 'conversation:' + conversationId;
766
+ }
767
+ return undefined;
768
+ }
769
+ function isStopMessageStateActive(raw) {
770
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
771
+ return false;
772
+ }
773
+ const record = raw;
774
+ const text = typeof record.stopMessageText === 'string' ? record.stopMessageText.trim() : '';
775
+ const maxRepeats = typeof record.stopMessageMaxRepeats === 'number' && Number.isFinite(record.stopMessageMaxRepeats)
776
+ ? Math.max(1, Math.floor(record.stopMessageMaxRepeats))
777
+ : 0;
778
+ const stageMode = typeof record.stopMessageStageMode === 'string' ? record.stopMessageStageMode.trim().toLowerCase() : '';
779
+ if (stageMode === 'off') {
780
+ return false;
781
+ }
782
+ if (text && maxRepeats > 0) {
783
+ return true;
784
+ }
785
+ return stageMode === 'on' || stageMode === 'auto';
786
+ }
787
+ function buildContinueExecutionOperations(metadata) {
788
+ const rt = readRuntimeMetadata(metadata);
789
+ if (rt?.serverToolFollowup === true) {
790
+ return [];
791
+ }
792
+ const sessionId = readString(metadata.sessionId);
793
+ const hasSessionId = Boolean(sessionId && sessionId.trim());
794
+ if (hasActiveStopMessageStateForContinueExecution(metadata)) {
795
+ logContinueExecution('skip_schema_due_stopmessage', { hasSessionId });
796
+ return [];
797
+ }
798
+ logContinueExecution('inject_schema', { hasSessionId });
799
+ const parameters = {
800
+ type: 'object',
801
+ properties: {},
802
+ required: [],
803
+ additionalProperties: false
804
+ };
805
+ const continueTool = {
806
+ type: 'function',
807
+ function: {
808
+ name: 'continue_execution',
809
+ description: 'No-op control tool. Use this before status summaries or before ending the turn. If you are about to send finish_reason=stop (or protocol-equivalent end_turn), do not end directly; emit a continue_execution tool call instead. Keep it lightweight. The server will auto-followup so execution continues, avoiding interruption and improving execution efficiency.',
810
+ parameters,
811
+ strict: true
812
+ }
813
+ };
814
+ return [
815
+ {
816
+ op: 'set_request_metadata_fields',
817
+ fields: { continueExecutionEnabled: true }
818
+ },
819
+ { op: 'append_tool_if_missing', toolName: 'continue_execution', tool: continueTool }
820
+ ];
821
+ }
822
+ function injectContinueExecutionDirectiveIntoUserMessage(request, metadata) {
823
+ const rt = readRuntimeMetadata(metadata);
824
+ if (rt?.serverToolFollowup === true) {
825
+ return request;
826
+ }
827
+ if (hasActiveStopMessageStateForContinueExecution(metadata)) {
828
+ const sessionId = readString(metadata.sessionId);
829
+ const hasSessionId = Boolean(sessionId && sessionId.trim());
830
+ logContinueExecution('skip_user_directive_due_stopmessage', { hasSessionId });
831
+ return request;
832
+ }
833
+ const messages = Array.isArray(request.messages) ? request.messages : [];
834
+ if (messages.length === 0) {
835
+ return request;
836
+ }
837
+ const hasDirective = messages.some((message) => {
838
+ if (!message || typeof message !== 'object' || message.role !== 'user') {
839
+ return false;
840
+ }
841
+ return messageContentContainsDirectiveMarker(message.content);
842
+ });
843
+ if (hasDirective) {
844
+ return request;
845
+ }
846
+ const lastUserIdx = findLastUserMessageIndex(messages);
847
+ if (lastUserIdx < 0) {
848
+ return request;
849
+ }
850
+ const nextMessages = messages.slice();
851
+ const target = nextMessages[lastUserIdx];
852
+ const nextContent = appendDirectiveToUserContent(target.content, CONTINUE_EXECUTION_SYSTEM_DIRECTIVE);
853
+ nextMessages[lastUserIdx] = {
854
+ ...target,
855
+ content: nextContent
856
+ };
857
+ const sessionId = readString(metadata.sessionId);
858
+ const hasSessionId = Boolean(sessionId && sessionId.trim());
859
+ logContinueExecution('inject_user_directive', { hasSessionId });
860
+ return {
861
+ ...request,
862
+ messages: nextMessages
863
+ };
864
+ }
865
+ function messageContentContainsDirectiveMarker(content) {
866
+ if (typeof content === 'string') {
867
+ return content.includes(CONTINUE_EXECUTION_SYSTEM_DIRECTIVE_MARKER);
868
+ }
869
+ if (!Array.isArray(content)) {
870
+ return false;
871
+ }
872
+ for (const part of content) {
873
+ if (!part || typeof part !== 'object') {
874
+ continue;
875
+ }
876
+ const text = typeof part.text === 'string' ? String(part.text) : '';
877
+ if (text.includes(CONTINUE_EXECUTION_SYSTEM_DIRECTIVE_MARKER)) {
878
+ return true;
879
+ }
880
+ }
881
+ return false;
882
+ }
883
+ function appendDirectiveToUserContent(content, directive) {
884
+ if (typeof content === 'string') {
885
+ const base = content.trimEnd();
886
+ return base.length ? `${base}\n\n${directive}` : directive;
887
+ }
888
+ if (!Array.isArray(content)) {
889
+ return directive;
890
+ }
891
+ const next = content.slice();
892
+ for (let idx = next.length - 1; idx >= 0; idx -= 1) {
893
+ const part = next[idx];
894
+ if (!part || typeof part !== 'object' || typeof part.text !== 'string') {
895
+ continue;
896
+ }
897
+ const base = String(part.text).trimEnd();
898
+ next[idx] = {
899
+ ...part,
900
+ text: base.length ? `${base}\n\n${directive}` : directive
901
+ };
902
+ return next;
903
+ }
904
+ next.push({ type: 'input_text', text: directive });
905
+ return next;
906
+ }
724
907
  function resolveSessionIdForClock(metadata, request) {
725
908
  const candidate = readString(metadata.sessionId) ?? readString(request.metadata?.sessionId);
726
909
  return candidate && candidate.trim() ? candidate.trim() : null;
@@ -736,6 +919,219 @@ function stripClockClearDirectiveFromText(text) {
736
919
  const next = replaced.replace(/\n{3,}/g, '\n\n').trim();
737
920
  return { hadClear: true, next };
738
921
  }
922
+ function unquoteMarkerToken(value) {
923
+ const trimmed = String(value || '').trim();
924
+ if (!trimmed) {
925
+ return '';
926
+ }
927
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
928
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
929
+ return trimmed.slice(1, -1).trim();
930
+ }
931
+ return trimmed;
932
+ }
933
+ function parseMarkerRecurrenceKind(raw) {
934
+ const value = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
935
+ if (!value) {
936
+ return undefined;
937
+ }
938
+ if (value === 'daily' || value === 'day') {
939
+ return 'daily';
940
+ }
941
+ if (value === 'weekly' || value === 'week') {
942
+ return 'weekly';
943
+ }
944
+ if (value === 'interval' || value === 'every_minutes' || value === 'every-minutes' || value === 'everyminutes') {
945
+ return 'interval';
946
+ }
947
+ return undefined;
948
+ }
949
+ function parseMarkerRecurrence(record) {
950
+ const recurrenceRaw = record.recurrence ?? record.repeat ?? record.cycle;
951
+ if (recurrenceRaw === undefined || recurrenceRaw === null || recurrenceRaw === false) {
952
+ return undefined;
953
+ }
954
+ let kind;
955
+ let everyMinutesRaw;
956
+ let maxRunsRaw;
957
+ if (typeof recurrenceRaw === 'string') {
958
+ kind = parseMarkerRecurrenceKind(recurrenceRaw);
959
+ everyMinutesRaw = record.everyMinutes;
960
+ maxRunsRaw = record.maxRuns;
961
+ }
962
+ else if (recurrenceRaw && typeof recurrenceRaw === 'object' && !Array.isArray(recurrenceRaw)) {
963
+ const recurrenceRecord = recurrenceRaw;
964
+ kind = parseMarkerRecurrenceKind(recurrenceRecord.kind ?? recurrenceRecord.type ?? recurrenceRecord.mode ?? recurrenceRecord.every);
965
+ everyMinutesRaw = recurrenceRecord.everyMinutes ?? recurrenceRecord.minutes ?? record.everyMinutes;
966
+ maxRunsRaw = recurrenceRecord.maxRuns ?? record.maxRuns;
967
+ }
968
+ if (!kind) {
969
+ return undefined;
970
+ }
971
+ const maxRuns = Number(maxRunsRaw);
972
+ if (!Number.isFinite(maxRuns) || Math.floor(maxRuns) <= 0) {
973
+ return undefined;
974
+ }
975
+ if (kind === 'interval') {
976
+ const everyMinutes = Number(everyMinutesRaw);
977
+ if (!Number.isFinite(everyMinutes) || Math.floor(everyMinutes) <= 0) {
978
+ return undefined;
979
+ }
980
+ return { kind: 'interval', maxRuns: Math.floor(maxRuns), everyMinutes: Math.floor(everyMinutes) };
981
+ }
982
+ return { kind, maxRuns: Math.floor(maxRuns) };
983
+ }
984
+ function parseClockScheduleDirectivePayload(payload) {
985
+ const raw = String(payload || '').trim();
986
+ if (!raw || /^clear$/i.test(raw)) {
987
+ return null;
988
+ }
989
+ const parseFromRecord = (record) => {
990
+ const timeRaw = typeof record.time === 'string' ? record.time :
991
+ typeof record.dueAt === 'string' ? record.dueAt :
992
+ typeof record.due_at === 'string' ? record.due_at :
993
+ '';
994
+ const messageRaw = typeof record.message === 'string' ? record.message :
995
+ typeof record.task === 'string' ? record.task :
996
+ '';
997
+ const dueAt = String(timeRaw || '').trim();
998
+ const task = String(messageRaw || '').trim();
999
+ const dueAtMs = parseDueAtMs(dueAt);
1000
+ if (!dueAtMs || !task) {
1001
+ return null;
1002
+ }
1003
+ const hasRecurrenceRaw = Object.prototype.hasOwnProperty.call(record, 'recurrence')
1004
+ || Object.prototype.hasOwnProperty.call(record, 'repeat')
1005
+ || Object.prototype.hasOwnProperty.call(record, 'cycle');
1006
+ const recurrence = parseMarkerRecurrence(record);
1007
+ if (hasRecurrenceRaw && !recurrence) {
1008
+ return null;
1009
+ }
1010
+ return {
1011
+ dueAtMs,
1012
+ dueAt: new Date(dueAtMs).toISOString(),
1013
+ task,
1014
+ ...(recurrence ? { recurrence } : {})
1015
+ };
1016
+ };
1017
+ try {
1018
+ const parsed = JSON.parse(raw);
1019
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
1020
+ const fromJson = parseFromRecord(parsed);
1021
+ if (fromJson) {
1022
+ return fromJson;
1023
+ }
1024
+ }
1025
+ }
1026
+ catch {
1027
+ // fall through to loose parser
1028
+ }
1029
+ const loose = /^\{\s*time\s*:\s*([^,}]+)\s*,\s*message\s*:\s*([^}]+)\s*\}$/i.exec(raw);
1030
+ if (!loose) {
1031
+ return null;
1032
+ }
1033
+ const dueAt = unquoteMarkerToken(loose[1]);
1034
+ const task = unquoteMarkerToken(loose[2]);
1035
+ const dueAtMs = parseDueAtMs(dueAt);
1036
+ if (!dueAtMs || !task) {
1037
+ return null;
1038
+ }
1039
+ return {
1040
+ dueAtMs,
1041
+ dueAt: new Date(dueAtMs).toISOString(),
1042
+ task
1043
+ };
1044
+ }
1045
+ function extractClockScheduleDirectivesFromText(text) {
1046
+ const pattern = /<\*\*\s*clock\s*:\s*([\s\S]*?)\s*\*\*>/gi;
1047
+ const directives = [];
1048
+ const next = String(text || '').replace(pattern, (full, payload) => {
1049
+ const parsed = parseClockScheduleDirectivePayload(String(payload || ''));
1050
+ if (!parsed) {
1051
+ return full;
1052
+ }
1053
+ directives.push(parsed);
1054
+ return '';
1055
+ });
1056
+ return {
1057
+ directives,
1058
+ next: next.replace(/\n{3,}/g, '\n\n').trim()
1059
+ };
1060
+ }
1061
+ function extractClockScheduleDirectivesFromContent(content) {
1062
+ if (typeof content === 'string') {
1063
+ const result = extractClockScheduleDirectivesFromText(content);
1064
+ return { directives: result.directives, next: result.next };
1065
+ }
1066
+ if (Array.isArray(content)) {
1067
+ const directives = [];
1068
+ const next = content.map((part) => {
1069
+ if (typeof part === 'string') {
1070
+ const result = extractClockScheduleDirectivesFromText(part);
1071
+ if (result.directives.length) {
1072
+ directives.push(...result.directives);
1073
+ }
1074
+ return result.next;
1075
+ }
1076
+ if (part && typeof part === 'object' && !Array.isArray(part)) {
1077
+ const block = part;
1078
+ const text = typeof block.text === 'string' ? block.text : undefined;
1079
+ if (!text) {
1080
+ return part;
1081
+ }
1082
+ const result = extractClockScheduleDirectivesFromText(text);
1083
+ if (result.directives.length) {
1084
+ directives.push(...result.directives);
1085
+ }
1086
+ return { ...block, text: result.next };
1087
+ }
1088
+ return part;
1089
+ });
1090
+ return { directives, next };
1091
+ }
1092
+ return { directives: [], next: content };
1093
+ }
1094
+ let clockMarkerCallSeq = 0;
1095
+ function buildClockMarkerCallId(requestId, index) {
1096
+ clockMarkerCallSeq += 1;
1097
+ const token = String(requestId || 'req').replace(/[^a-zA-Z0-9_-]+/g, '_') || 'req';
1098
+ return `call_clock_marker_${token}_${index + 1}_${clockMarkerCallSeq}`;
1099
+ }
1100
+ function buildClockMarkerScheduleMessages(requestId, markerIndex, marker, payload) {
1101
+ const callId = buildClockMarkerCallId(requestId, markerIndex);
1102
+ const args = {
1103
+ action: 'schedule',
1104
+ items: [
1105
+ {
1106
+ dueAt: marker.dueAt,
1107
+ task: marker.task,
1108
+ ...(marker.recurrence ? { recurrence: marker.recurrence } : {})
1109
+ }
1110
+ ]
1111
+ };
1112
+ return [
1113
+ {
1114
+ role: 'assistant',
1115
+ content: null,
1116
+ tool_calls: [
1117
+ {
1118
+ id: callId,
1119
+ type: 'function',
1120
+ function: {
1121
+ name: 'clock',
1122
+ arguments: JSON.stringify(args)
1123
+ }
1124
+ }
1125
+ ]
1126
+ },
1127
+ {
1128
+ role: 'tool',
1129
+ tool_call_id: callId,
1130
+ name: 'clock',
1131
+ content: JSON.stringify(payload)
1132
+ }
1133
+ ];
1134
+ }
739
1135
  function stripClockClearDirectiveFromContent(content) {
740
1136
  if (typeof content === 'string') {
741
1137
  const { hadClear, next } = stripClockClearDirectiveFromText(content);
@@ -781,7 +1177,8 @@ function findLastUserMessageIndex(messages) {
781
1177
  async function maybeInjectClockRemindersAndApplyDirectives(request, metadata, requestId) {
782
1178
  const rt = readRuntimeMetadata(metadata);
783
1179
  // Do not inject reminders or apply clock directives during internal servertool followup hops.
784
- if (rt?.serverToolFollowup === true) {
1180
+ const allowClockFollowupReminderInjection = rt?.clockFollowupInjectReminders === true;
1181
+ if (rt?.serverToolFollowup === true && !allowClockFollowupReminderInjection) {
785
1182
  return request;
786
1183
  }
787
1184
  const rawConfig = rt?.clock;
@@ -800,14 +1197,17 @@ async function maybeInjectClockRemindersAndApplyDirectives(request, metadata, re
800
1197
  const lastUserIdx = findLastUserMessageIndex(messages);
801
1198
  // 1) Apply <**clock:clear**> directive (latest user message only).
802
1199
  let hadClear = false;
1200
+ let clockScheduleDirectives = [];
803
1201
  let baseMessages = messages;
804
1202
  if (lastUserIdx >= 0) {
805
1203
  const lastUser = messages[lastUserIdx];
806
1204
  const stripped = stripClockClearDirectiveFromContent(lastUser.content);
807
1205
  hadClear = stripped.hadClear;
808
- if (hadClear) {
1206
+ const extracted = extractClockScheduleDirectivesFromContent(stripped.next);
1207
+ clockScheduleDirectives = extracted.directives;
1208
+ if (hadClear || clockScheduleDirectives.length > 0) {
809
1209
  baseMessages = messages.slice();
810
- baseMessages[lastUserIdx] = { ...lastUser, content: stripped.next };
1210
+ baseMessages[lastUserIdx] = { ...lastUser, content: extracted.next };
811
1211
  }
812
1212
  }
813
1213
  if (hadClear) {
@@ -822,7 +1222,54 @@ async function maybeInjectClockRemindersAndApplyDirectives(request, metadata, re
822
1222
  }
823
1223
  // Continue: still inject per-request time tag (but skip due reminders).
824
1224
  }
825
- // 2) Inject due reminders as a user message + attach reservation for response-side commit.
1225
+ // 2) Apply private schedule directives: <**clock:{time,message}**>
1226
+ // Convert to actual clock scheduling and append synthetic tool_call/tool_result messages
1227
+ // so downstream model sees canonical tool semantics.
1228
+ let markerToolMessages = [];
1229
+ if (clockScheduleDirectives.length > 0) {
1230
+ for (let index = 0; index < clockScheduleDirectives.length; index += 1) {
1231
+ const marker = clockScheduleDirectives[index];
1232
+ const itemBase = {
1233
+ dueAtMs: marker.dueAtMs,
1234
+ task: marker.task,
1235
+ ...(marker.recurrence ? { recurrence: marker.recurrence } : {})
1236
+ };
1237
+ const now = Date.now();
1238
+ const guardedItem = marker.dueAtMs <= now + clockConfig.dueWindowMs
1239
+ ? { ...itemBase, notBeforeRequestId: requestId }
1240
+ : itemBase;
1241
+ if (!sessionId) {
1242
+ markerToolMessages = markerToolMessages.concat(buildClockMarkerScheduleMessages(requestId, index, marker, {
1243
+ ok: false,
1244
+ action: 'schedule',
1245
+ message: 'clock requires sessionId (x-session-id header or metadata.sessionId).'
1246
+ }));
1247
+ continue;
1248
+ }
1249
+ try {
1250
+ const scheduled = await scheduleClockTasks(sessionId, [guardedItem], clockConfig);
1251
+ markerToolMessages = markerToolMessages.concat(buildClockMarkerScheduleMessages(requestId, index, marker, {
1252
+ ok: true,
1253
+ action: 'schedule',
1254
+ scheduled: scheduled.map((entry) => ({
1255
+ taskId: entry.taskId,
1256
+ dueAt: new Date(entry.dueAtMs).toISOString(),
1257
+ task: entry.task,
1258
+ deliveryCount: entry.deliveryCount
1259
+ }))
1260
+ }));
1261
+ logClock('schedule', { sessionId, count: scheduled.length, source: 'marker' });
1262
+ }
1263
+ catch (error) {
1264
+ markerToolMessages = markerToolMessages.concat(buildClockMarkerScheduleMessages(requestId, index, marker, {
1265
+ ok: false,
1266
+ action: 'schedule',
1267
+ message: `clock.schedule failed: ${error instanceof Error ? error.message : String(error ?? 'unknown')}`
1268
+ }));
1269
+ }
1270
+ }
1271
+ }
1272
+ // 3) Inject due reminders as a user message + attach reservation for response-side commit.
826
1273
  let reservation = null;
827
1274
  let dueInjectText = '';
828
1275
  if (!hadClear && sessionId) {
@@ -850,17 +1297,17 @@ async function maybeInjectClockRemindersAndApplyDirectives(request, metadata, re
850
1297
  content: [
851
1298
  '[Clock Reminder]: scheduled tasks are due.',
852
1299
  dueInjectText,
853
- 'You may call tools to complete these tasks. If the tool list is incomplete, standard tools have been injected.'
1300
+ 'You may call tools to complete these tasks. If the tool list is incomplete, standard tools have been injected. MANDATORY: if waiting is needed, use the clock tool to schedule wake-up (clock.schedule) now; do not only promise to wait.'
854
1301
  ].join('\n')
855
1302
  };
856
1303
  })();
857
- // 3) When we have due tasks, ensure a standard tool set is present (best-effort).
1304
+ // 4) When we have due tasks, ensure a standard tool set is present (best-effort).
858
1305
  let nextRequest = request;
859
1306
  if (dueUserMessage) {
860
1307
  const ensureToolsOps = buildClockStandardToolsOperations();
861
1308
  nextRequest = applyHubOperations(nextRequest, ensureToolsOps);
862
1309
  }
863
- // 4) Per-request time injection (user time tag or paired clock.get tool result).
1310
+ // 5) Per-request time injection (user time tag or paired clock.get tool result).
864
1311
  let snapshot = null;
865
1312
  try {
866
1313
  snapshot = await getClockTimeSnapshot();
@@ -882,7 +1329,10 @@ async function maybeInjectClockRemindersAndApplyDirectives(request, metadata, re
882
1329
  __clockReservation: reservation
883
1330
  }
884
1331
  : baseMetadata;
885
- const messagesWithDue = dueUserMessage ? [...baseMessages, dueUserMessage] : baseMessages.slice();
1332
+ const messagesWithMarkers = markerToolMessages.length > 0
1333
+ ? [...baseMessages, ...markerToolMessages]
1334
+ : baseMessages.slice();
1335
+ const messagesWithDue = dueUserMessage ? [...messagesWithMarkers, dueUserMessage] : messagesWithMarkers;
886
1336
  // Always inject time via user-role content to keep the tag visible without adding
887
1337
  // extra tool-call semantics that may distract the model.
888
1338
  //