@jsonstudio/llms 0.6.1449 → 0.6.1643

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 (71) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.js +6 -1
  2. package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.d.ts +4 -6
  3. package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.js +179 -41
  4. package/dist/conversion/compat/actions/antigravity-thought-signature-cache.js +73 -14
  5. package/dist/conversion/compat/actions/antigravity-thought-signature-prepare.js +165 -10
  6. package/dist/conversion/compat/actions/gemini-cli-request.js +72 -13
  7. package/dist/conversion/compat/antigravity-session-signature.d.ts +68 -1
  8. package/dist/conversion/compat/antigravity-session-signature.js +833 -21
  9. package/dist/conversion/compat/profiles/anthropic-claude-code.json +17 -0
  10. package/dist/conversion/compat/profiles/chat-gemini-cli.json +1 -0
  11. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +33 -8
  12. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +17 -1
  13. package/dist/conversion/hub/pipeline/compat/compat-profile-store.js +12 -3
  14. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +1 -0
  15. package/dist/conversion/hub/pipeline/hub-pipeline.js +24 -0
  16. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +20 -0
  17. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +26 -1
  18. package/dist/conversion/hub/process/chat-process.js +300 -67
  19. package/dist/conversion/hub/response/provider-response.js +4 -3
  20. package/dist/conversion/shared/gemini-tool-utils.js +134 -9
  21. package/dist/conversion/shared/text-markup-normalizer.js +90 -1
  22. package/dist/conversion/shared/thought-signature-validator.d.ts +1 -1
  23. package/dist/conversion/shared/thought-signature-validator.js +2 -1
  24. package/dist/quota/apikey-reset.d.ts +17 -0
  25. package/dist/quota/apikey-reset.js +43 -0
  26. package/dist/quota/index.d.ts +2 -0
  27. package/dist/quota/index.js +1 -0
  28. package/dist/quota/quota-manager.d.ts +44 -0
  29. package/dist/quota/quota-manager.js +491 -0
  30. package/dist/quota/quota-state.d.ts +6 -0
  31. package/dist/quota/quota-state.js +167 -0
  32. package/dist/quota/types.d.ts +61 -0
  33. package/dist/quota/types.js +1 -0
  34. package/dist/router/virtual-router/bootstrap.js +103 -6
  35. package/dist/router/virtual-router/engine-health.js +104 -0
  36. package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +18 -0
  37. package/dist/router/virtual-router/engine-selection/tier-priority.d.ts +1 -2
  38. package/dist/router/virtual-router/engine-selection/tier-priority.js +2 -2
  39. package/dist/router/virtual-router/engine-selection/tier-selection-select.js +34 -10
  40. package/dist/router/virtual-router/engine-selection/tier-selection.js +250 -6
  41. package/dist/router/virtual-router/engine-selection.js +2 -2
  42. package/dist/router/virtual-router/engine.d.ts +16 -1
  43. package/dist/router/virtual-router/engine.js +320 -42
  44. package/dist/router/virtual-router/features.js +20 -2
  45. package/dist/router/virtual-router/success-center.d.ts +10 -0
  46. package/dist/router/virtual-router/success-center.js +32 -0
  47. package/dist/router/virtual-router/types.d.ts +48 -0
  48. package/dist/servertool/clock/config.d.ts +2 -0
  49. package/dist/servertool/clock/config.js +10 -2
  50. package/dist/servertool/clock/daemon.js +3 -0
  51. package/dist/servertool/clock/ntp.d.ts +18 -0
  52. package/dist/servertool/clock/ntp.js +318 -0
  53. package/dist/servertool/clock/paths.d.ts +1 -0
  54. package/dist/servertool/clock/paths.js +3 -0
  55. package/dist/servertool/clock/state.d.ts +2 -0
  56. package/dist/servertool/clock/state.js +15 -2
  57. package/dist/servertool/clock/tasks.d.ts +1 -0
  58. package/dist/servertool/clock/tasks.js +24 -1
  59. package/dist/servertool/clock/types.d.ts +21 -0
  60. package/dist/servertool/engine.js +105 -1
  61. package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.d.ts +1 -0
  62. package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.js +201 -0
  63. package/dist/servertool/handlers/clock-auto.js +39 -4
  64. package/dist/servertool/handlers/clock.js +145 -16
  65. package/dist/servertool/handlers/followup-request-builder.js +84 -0
  66. package/dist/servertool/handlers/stop-message-auto.js +1 -1
  67. package/dist/servertool/server-side-tools.d.ts +1 -0
  68. package/dist/servertool/server-side-tools.js +1 -0
  69. package/dist/servertool/types.d.ts +2 -0
  70. package/dist/tools/apply-patch/execution-capturer.js +24 -3
  71. package/package.json +3 -2
@@ -0,0 +1,201 @@
1
+ import { registerServerToolHandler } from '../registry.js';
2
+ import { extractCapturedChatSeed } from './followup-request-builder.js';
3
+ import { ensureRuntimeMetadata, readRuntimeMetadata } from '../../conversion/shared/runtime-metadata.js';
4
+ import { cloneJson } from '../server-side-tools.js';
5
+ const FLOW_ID = 'antigravity_thought_signature_bootstrap';
6
+ function readProviderKey(adapterContext) {
7
+ if (!adapterContext || typeof adapterContext !== 'object') {
8
+ return '';
9
+ }
10
+ const raw = adapterContext.providerKey ?? adapterContext.runtimeKey ?? adapterContext.providerId;
11
+ return typeof raw === 'string' ? raw.trim() : '';
12
+ }
13
+ function isAntigravityFamily(providerKey) {
14
+ const lowered = providerKey.toLowerCase();
15
+ return lowered.startsWith('antigravity.') || lowered.startsWith('gemini-cli.');
16
+ }
17
+ function readErrorInfo(base) {
18
+ const err = base.error;
19
+ if (!err || typeof err !== 'object' || Array.isArray(err)) {
20
+ return null;
21
+ }
22
+ const codeRaw = err.code;
23
+ const msgRaw = err.message;
24
+ const statusRaw = err.status ?? err.statusCode;
25
+ const code = typeof codeRaw === 'string' ? codeRaw.trim() : typeof codeRaw === 'number' ? String(codeRaw) : undefined;
26
+ const message = typeof msgRaw === 'string' ? msgRaw.trim() : undefined;
27
+ const status = typeof statusRaw === 'number' && Number.isFinite(statusRaw)
28
+ ? Math.floor(statusRaw)
29
+ : typeof code === 'string' && /^HTTP_\d{3}$/i.test(code)
30
+ ? Number(code.split('_')[1])
31
+ : typeof code === 'string' && /^\d{3}$/.test(code)
32
+ ? Number(code)
33
+ : undefined;
34
+ return { ...(status ? { status } : {}), ...(code ? { code } : {}), ...(message ? { message } : {}) };
35
+ }
36
+ function isSignatureInvalidError(error) {
37
+ const code = (error.code || '').toLowerCase();
38
+ if (code.includes('signature')) {
39
+ return true;
40
+ }
41
+ const msg = (error.message || '').toLowerCase();
42
+ return msg.includes('signature') && (msg.includes('invalid') || msg.includes('corrupt') || msg.includes('validator'));
43
+ }
44
+ function shouldTriggerBootstrap(error) {
45
+ // One-shot bootstrap trigger:
46
+ // - Always attempt once on 429 (may be quota OR signature validator; one-shot prevents loops).
47
+ // - Also attempt on 400 when we can confidently classify as signature invalid/missing.
48
+ if (error.status === 429)
49
+ return true;
50
+ if (error.status === 400 && isSignatureInvalidError(error))
51
+ return true;
52
+ return false;
53
+ }
54
+ function buildClockToolSchema() {
55
+ // Keep this schema aligned with docs/SERVERTOOL_CLOCK_DESIGN.md + chat-process injection.
56
+ return {
57
+ type: 'function',
58
+ function: {
59
+ name: 'clock',
60
+ description: 'Time + Alarm for this session. Use get/schedule/list/cancel/clear. Scheduled reminders will be injected into future requests.',
61
+ strict: true,
62
+ parameters: {
63
+ type: 'object',
64
+ properties: {
65
+ action: {
66
+ type: 'string',
67
+ enum: ['get', 'schedule', 'list', 'cancel', 'clear'],
68
+ description: 'Get current time, or schedule/list/cancel/clear session-scoped reminders.'
69
+ },
70
+ items: {
71
+ type: 'array',
72
+ description: 'For schedule: list of reminders to add.',
73
+ items: {
74
+ type: 'object',
75
+ properties: {
76
+ dueAt: {
77
+ type: 'string',
78
+ description: 'ISO8601 datetime with timezone (e.g. 2026-01-21T20:30:00-08:00).'
79
+ },
80
+ task: {
81
+ type: 'string',
82
+ description: 'Reminder text (should include which tool to use and what to do).'
83
+ },
84
+ tool: {
85
+ type: 'string',
86
+ description: 'Optional suggested tool name (hint only).'
87
+ },
88
+ arguments: {
89
+ type: 'string',
90
+ description: 'Optional suggested tool arguments as a JSON string (hint only). Use "{}" when unsure.'
91
+ }
92
+ },
93
+ required: ['dueAt', 'task', 'tool', 'arguments'],
94
+ additionalProperties: false
95
+ }
96
+ },
97
+ taskId: {
98
+ type: 'string',
99
+ description: 'For cancel: taskId to remove.'
100
+ }
101
+ },
102
+ required: ['action', 'items', 'taskId'],
103
+ additionalProperties: false
104
+ }
105
+ }
106
+ };
107
+ }
108
+ function ensureClockTool(tools) {
109
+ const list = Array.isArray(tools) ? cloneJson(tools) : [];
110
+ const hasClock = list.some((tool) => {
111
+ const fn = tool && typeof tool === 'object' && !Array.isArray(tool) ? tool.function : undefined;
112
+ const name = fn && typeof fn === 'object' && typeof fn.name === 'string' ? String(fn.name) : '';
113
+ return name.trim() === 'clock';
114
+ });
115
+ if (hasClock) {
116
+ return list;
117
+ }
118
+ return [...list, buildClockToolSchema()];
119
+ }
120
+ const BOOTSTRAP_USER_PROMPT = '请先调用 `clock` 工具并传入 `{\"action\":\"get\",\"items\":[],\"taskId\":\"\"}` 获取当前时间;' +
121
+ '得到工具返回后只需回复 `OK`(不要调用其它工具)。';
122
+ const handler = async (ctx) => {
123
+ if (!ctx.capabilities.reenterPipeline) {
124
+ return null;
125
+ }
126
+ if (ctx.providerProtocol !== 'gemini-chat') {
127
+ return null;
128
+ }
129
+ const providerKey = readProviderKey(ctx.adapterContext);
130
+ if (!providerKey || !isAntigravityFamily(providerKey)) {
131
+ return null;
132
+ }
133
+ const rt = readRuntimeMetadata(ctx.adapterContext);
134
+ if (rt?.serverToolFollowup === true) {
135
+ return null;
136
+ }
137
+ if (rt?.antigravityThoughtSignatureBootstrapAttempted === true) {
138
+ return null;
139
+ }
140
+ const error = readErrorInfo(ctx.base);
141
+ if (!error || !shouldTriggerBootstrap(error)) {
142
+ return null;
143
+ }
144
+ const captured = ctx.adapterContext?.capturedChatRequest;
145
+ const seed = extractCapturedChatSeed(captured);
146
+ if (!seed) {
147
+ return null;
148
+ }
149
+ // Preflight bootstrap request:
150
+ // - Avoid any historical tool calls so Gemini can emit a fresh thoughtSignature.
151
+ // - Keep the FIRST user message identical to the original request to preserve derived session_id.
152
+ const originalMessages = Array.isArray(seed.messages) ? cloneJson(seed.messages) : [];
153
+ const firstUser = originalMessages.find((m) => {
154
+ const role = typeof m?.role === 'string' ? String(m.role).toLowerCase() : '';
155
+ return role === 'user';
156
+ });
157
+ if (!firstUser) {
158
+ return null;
159
+ }
160
+ const messages = [cloneJson(firstUser), { role: 'user', content: BOOTSTRAP_USER_PROMPT }];
161
+ const parameters = {
162
+ ...(seed.parameters && typeof seed.parameters === 'object' && !Array.isArray(seed.parameters)
163
+ ? cloneJson(seed.parameters)
164
+ : {})
165
+ };
166
+ // Gemini toolConfig forcing: request the model to emit a clock call first, before any other tools.
167
+ parameters.tool_config = {
168
+ functionCallingConfig: {
169
+ mode: 'ANY',
170
+ allowedFunctionNames: ['clock']
171
+ }
172
+ };
173
+ const followupPayload = {
174
+ ...(seed.model ? { model: seed.model } : {}),
175
+ messages,
176
+ tools: ensureClockTool([]),
177
+ ...(Object.keys(parameters).length ? { parameters } : {})
178
+ };
179
+ const followupMetadata = {
180
+ __shadowCompareForcedProviderKey: providerKey
181
+ };
182
+ const followupRt = ensureRuntimeMetadata(followupMetadata);
183
+ followupRt.antigravityThoughtSignatureBootstrap = true;
184
+ followupRt.antigravityThoughtSignatureBootstrapAttempted = true;
185
+ return {
186
+ flowId: FLOW_ID,
187
+ finalize: async () => ({
188
+ chatResponse: ctx.base,
189
+ execution: {
190
+ flowId: FLOW_ID,
191
+ followup: {
192
+ requestIdSuffix: ':antigravity_ts_bootstrap',
193
+ entryEndpoint: ctx.entryEndpoint,
194
+ payload: followupPayload,
195
+ metadata: followupMetadata
196
+ }
197
+ }
198
+ })
199
+ };
200
+ };
201
+ registerServerToolHandler(FLOW_ID, handler, { trigger: 'auto' });
@@ -2,6 +2,7 @@ import { registerServerToolHandler } from '../registry.js';
2
2
  import { extractCapturedChatSeed } from './followup-request-builder.js';
3
3
  import { readRuntimeMetadata } from '../../conversion/shared/runtime-metadata.js';
4
4
  import { findNextUndeliveredDueAtMs, listClockTasks, resolveClockConfig, startClockDaemonIfNeeded } from '../clock/task-store.js';
5
+ import { nowMs } from '../clock/state.js';
5
6
  import { logClock } from '../clock/log.js';
6
7
  const FLOW_ID = 'clock_hold_flow';
7
8
  function resolveClientConnectionState(value) {
@@ -14,6 +15,23 @@ function resolveClientConnectionState(value) {
14
15
  }
15
16
  return { disconnected: record.disconnected };
16
17
  }
18
+ function clientWantsStreaming(adapterContext) {
19
+ if (!adapterContext || typeof adapterContext !== 'object' || Array.isArray(adapterContext)) {
20
+ return false;
21
+ }
22
+ const record = adapterContext;
23
+ if (record.stream === true) {
24
+ return true;
25
+ }
26
+ const clientHeaders = record.clientHeaders;
27
+ if (clientHeaders && typeof clientHeaders === 'object' && !Array.isArray(clientHeaders)) {
28
+ const accept = clientHeaders.accept;
29
+ if (typeof accept === 'string' && accept.toLowerCase().includes('text/event-stream')) {
30
+ return true;
31
+ }
32
+ }
33
+ return false;
34
+ }
17
35
  function isStopFinishReason(base) {
18
36
  if (!base || typeof base !== 'object' || Array.isArray(base)) {
19
37
  return false;
@@ -109,30 +127,47 @@ const handler = async (ctx) => {
109
127
  return null;
110
128
  }
111
129
  await startClockDaemonIfNeeded(clockConfig);
130
+ // IMPORTANT: clock hold requires a long-lived client connection.
131
+ // - Streaming/SSE clients: ok to hold (keepalive is handled by host SSE bridge).
132
+ // - Non-streaming/JSON clients: only hold when explicitly enabled via config,
133
+ // and only within a small max window (holdMaxMs).
134
+ const wantsStream = clientWantsStreaming(ctx.adapterContext);
135
+ if (!wantsStream && clockConfig.holdNonStreaming !== true) {
136
+ return null;
137
+ }
112
138
  const seed = extractCapturedChatSeed(record.capturedChatRequest);
113
139
  if (!seed) {
114
140
  return null;
115
141
  }
116
142
  const tasks = await listClockTasks(sessionId, clockConfig);
117
- const at = Date.now();
143
+ const at = nowMs();
118
144
  const nextDueAtMs = findNextUndeliveredDueAtMs(tasks, at);
119
145
  if (!nextDueAtMs) {
120
146
  return null;
121
147
  }
122
148
  // Wait until the "due window" is reached (now >= dueAt - dueWindowMs).
123
149
  const thresholdMs = nextDueAtMs - clockConfig.dueWindowMs;
150
+ // Important: if we're already inside the due window, do NOT auto-followup in the current request
151
+ // (prevents same-request loops; the reminder will be injected on the next request).
152
+ if (at >= thresholdMs) {
153
+ return null;
154
+ }
155
+ const remainingMs = thresholdMs - at;
156
+ if (clockConfig.holdMaxMs >= 0 && remainingMs > clockConfig.holdMaxMs) {
157
+ return null;
158
+ }
124
159
  logClock('hold_start', { sessionId, nextDueAtMs, thresholdMs });
125
- while (Date.now() < thresholdMs) {
160
+ while (nowMs() < thresholdMs) {
126
161
  const state = resolveClientConnectionState(ctx.adapterContext.clientConnectionState);
127
162
  if (state?.disconnected === true) {
128
163
  return null;
129
164
  }
130
- const remaining = thresholdMs - Date.now();
165
+ const remaining = thresholdMs - nowMs();
131
166
  await sleep(computeHoldSleepMs(remaining));
132
167
  // Best-effort: if tasks were cleared/cancelled while holding, stop holding.
133
168
  try {
134
169
  const refreshed = await listClockTasks(sessionId, clockConfig);
135
- const refreshedNext = findNextUndeliveredDueAtMs(refreshed, Date.now());
170
+ const refreshedNext = findNextUndeliveredDueAtMs(refreshed, nowMs());
136
171
  if (!refreshedNext) {
137
172
  return null;
138
173
  }
@@ -1,10 +1,63 @@
1
1
  import { registerServerToolHandler } from '../registry.js';
2
2
  import { cloneJson } from '../server-side-tools.js';
3
3
  import { extractCapturedChatSeed } from './followup-request-builder.js';
4
- import { readRuntimeMetadata } from '../../conversion/shared/runtime-metadata.js';
4
+ import { ensureRuntimeMetadata, readRuntimeMetadata } from '../../conversion/shared/runtime-metadata.js';
5
5
  import { cancelClockTask, clearClockTasks, listClockTasks, resolveClockConfig, parseDueAtMs, scheduleClockTasks, startClockDaemonIfNeeded } from '../clock/task-store.js';
6
+ import { getClockTimeSnapshot } from '../clock/ntp.js';
7
+ import { nowMs } from '../clock/state.js';
6
8
  import { logClock } from '../clock/log.js';
7
9
  const FLOW_ID = 'clock_flow';
10
+ function extractAssistantMessageFromChatLike(chatResponse) {
11
+ if (!chatResponse || typeof chatResponse !== 'object') {
12
+ return null;
13
+ }
14
+ const choices = Array.isArray(chatResponse.choices) ? chatResponse.choices : [];
15
+ if (!choices.length) {
16
+ return null;
17
+ }
18
+ const first = choices[0];
19
+ if (!first || typeof first !== 'object' || Array.isArray(first)) {
20
+ return null;
21
+ }
22
+ const message = first.message;
23
+ if (!message || typeof message !== 'object' || Array.isArray(message)) {
24
+ return null;
25
+ }
26
+ const role = typeof message.role === 'string' ? String(message.role).toLowerCase() : '';
27
+ if (role && role !== 'assistant') {
28
+ return null;
29
+ }
30
+ return cloneJson(message);
31
+ }
32
+ function buildToolMessagesFromToolOutputs(chatResponse) {
33
+ const outputs = Array.isArray(chatResponse.tool_outputs) ? chatResponse.tool_outputs : [];
34
+ const out = [];
35
+ for (const entry of outputs) {
36
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
37
+ continue;
38
+ const toolCallId = typeof entry.tool_call_id === 'string' ? String(entry.tool_call_id) : '';
39
+ if (!toolCallId)
40
+ continue;
41
+ const name = typeof entry.name === 'string' && String(entry.name).trim()
42
+ ? String(entry.name).trim()
43
+ : 'tool';
44
+ const rawContent = entry.content;
45
+ let contentText;
46
+ if (typeof rawContent === 'string') {
47
+ contentText = rawContent;
48
+ }
49
+ else {
50
+ try {
51
+ contentText = JSON.stringify(rawContent ?? {});
52
+ }
53
+ catch {
54
+ contentText = String(rawContent ?? '');
55
+ }
56
+ }
57
+ out.push({ role: 'tool', tool_call_id: toolCallId, name, content: contentText });
58
+ }
59
+ return out;
60
+ }
8
61
  function parseToolArguments(toolCall) {
9
62
  if (!toolCall.arguments || typeof toolCall.arguments !== 'string') {
10
63
  return {};
@@ -59,7 +112,7 @@ function asPlainObject(value) {
59
112
  }
60
113
  function normalizeAction(value) {
61
114
  const raw = typeof value === 'string' ? value.trim().toLowerCase() : '';
62
- if (raw === 'list' || raw === 'cancel' || raw === 'clear' || raw === 'schedule') {
115
+ if (raw === 'get' || raw === 'list' || raw === 'cancel' || raw === 'clear' || raw === 'schedule') {
63
116
  return raw;
64
117
  }
65
118
  return 'schedule';
@@ -147,6 +200,12 @@ const handler = async (ctx) => {
147
200
  const patched = injectClockToolOutput(ctx.base, toolCall, payload);
148
201
  const seed = extractCapturedChatSeed(ctx.adapterContext?.capturedChatRequest);
149
202
  const canFollowup = Boolean(seed);
203
+ const bootstrapActive = rt?.antigravityThoughtSignatureBootstrap === true;
204
+ const forcedProviderKey = bootstrapActive &&
205
+ typeof ctx.adapterContext.providerKey === 'string' &&
206
+ String(ctx.adapterContext.providerKey).trim()
207
+ ? String(ctx.adapterContext.providerKey).trim()
208
+ : '';
150
209
  return {
151
210
  chatResponse: patched,
152
211
  execution: {
@@ -156,12 +215,46 @@ const handler = async (ctx) => {
156
215
  followup: {
157
216
  requestIdSuffix: ':clock_followup',
158
217
  entryEndpoint: ctx.entryEndpoint,
159
- injection: {
160
- ops: [
161
- { op: 'append_assistant_message', required: true },
162
- { op: 'append_tool_messages_from_tool_outputs', required: true }
163
- ]
164
- }
218
+ ...(bootstrapActive && seed
219
+ ? {
220
+ payload: (() => {
221
+ const messages = Array.isArray(seed.messages) ? cloneJson(seed.messages) : [];
222
+ const assistant = extractAssistantMessageFromChatLike(ctx.base);
223
+ if (assistant) {
224
+ messages.push(assistant);
225
+ }
226
+ messages.push(...buildToolMessagesFromToolOutputs(patched));
227
+ const params = seed.parameters && typeof seed.parameters === 'object' && !Array.isArray(seed.parameters)
228
+ ? { ...seed.parameters }
229
+ : {};
230
+ // Bootstrap-only: the first hop forces tool_config=clock; the second hop must clear it
231
+ // so the model can either answer or call other tools normally.
232
+ delete params.tool_config;
233
+ return {
234
+ ...(seed.model ? { model: seed.model } : {}),
235
+ messages,
236
+ ...(Array.isArray(seed.tools) ? { tools: cloneJson(seed.tools) } : {}),
237
+ ...(Object.keys(params).length ? { parameters: params } : {})
238
+ };
239
+ })(),
240
+ metadata: (() => {
241
+ const meta = {};
242
+ if (forcedProviderKey) {
243
+ meta.__shadowCompareForcedProviderKey = forcedProviderKey;
244
+ }
245
+ const runtime = ensureRuntimeMetadata(meta);
246
+ runtime.antigravityThoughtSignatureBootstrapAttempted = true;
247
+ return meta;
248
+ })()
249
+ }
250
+ : {
251
+ injection: {
252
+ ops: [
253
+ { op: 'append_assistant_message', required: true },
254
+ { op: 'append_tool_messages_from_tool_outputs', required: true }
255
+ ]
256
+ }
257
+ })
165
258
  }
166
259
  }
167
260
  : {})
@@ -170,23 +263,47 @@ const handler = async (ctx) => {
170
263
  }
171
264
  };
172
265
  };
173
- if (!sessionId) {
174
- logClock('missing_session', { action });
266
+ if (!clockConfig) {
267
+ logClock('disabled', { action, hasSessionId: true });
175
268
  return respond({
176
269
  ok: false,
177
270
  action,
178
- message: 'clock requires sessionId (x-session-id header or metadata.sessionId).'
271
+ message: 'clock tool is not enabled (virtualrouter.clock.enabled=true required).'
179
272
  });
180
273
  }
181
- if (!clockConfig) {
182
- logClock('disabled', { action, hasSessionId: true });
274
+ await startClockDaemonIfNeeded(clockConfig);
275
+ if (action === 'get') {
276
+ try {
277
+ const snapshot = await getClockTimeSnapshot();
278
+ logClock('get', { hasSessionId: Boolean(sessionId) });
279
+ return respond({
280
+ ok: true,
281
+ action,
282
+ active: true,
283
+ nowMs: snapshot.nowMs,
284
+ utc: snapshot.utc,
285
+ local: snapshot.local,
286
+ timezone: snapshot.timezone,
287
+ ntp: snapshot.ntp
288
+ });
289
+ }
290
+ catch (err) {
291
+ logClock('get_error', { message: String(err?.message || err || '') });
292
+ return respond({
293
+ ok: false,
294
+ action,
295
+ message: `clock.get failed: ${String(err?.message || err || 'unknown')}`
296
+ });
297
+ }
298
+ }
299
+ if (!sessionId) {
300
+ logClock('missing_session', { action });
183
301
  return respond({
184
302
  ok: false,
185
303
  action,
186
- message: 'clock tool is not enabled (virtualrouter.clock.enabled=true required).'
304
+ message: 'clock requires sessionId (x-session-id header or metadata.sessionId).'
187
305
  });
188
306
  }
189
- await startClockDaemonIfNeeded(clockConfig);
190
307
  if (action === 'list') {
191
308
  const items = await listClockTasks(sessionId, clockConfig);
192
309
  logClock('list', { sessionId, count: items.length });
@@ -212,7 +329,19 @@ const handler = async (ctx) => {
212
329
  logClock('schedule_invalid', { sessionId, message: normalized.message ?? 'invalid schedule items' });
213
330
  return respond({ ok: false, action, message: normalized.message ?? 'invalid schedule items' });
214
331
  }
215
- const scheduled = await scheduleClockTasks(sessionId, normalized.items, clockConfig);
332
+ const at = nowMs();
333
+ const guardedItems = normalized.items.map((item) => {
334
+ if (!item || typeof item !== 'object')
335
+ return item;
336
+ if (!Number.isFinite(item.dueAtMs))
337
+ return item;
338
+ // When dueAt is already within the trigger window, do NOT allow same-request injection.
339
+ if (item.dueAtMs <= at + clockConfig.dueWindowMs) {
340
+ return { ...item, notBeforeRequestId: ctx.requestId };
341
+ }
342
+ return item;
343
+ });
344
+ const scheduled = await scheduleClockTasks(sessionId, guardedItems, clockConfig);
216
345
  logClock('schedule', { sessionId, count: scheduled.length });
217
346
  return respond({
218
347
  ok: true,
@@ -175,6 +175,7 @@ function injectVisionSummaryIntoMessages(source, summary) {
175
175
  : '';
176
176
  if (typeValue.includes('image')) {
177
177
  removed = true;
178
+ nextParts.push({ type: 'text', text: '[Image omitted]' });
178
179
  continue;
179
180
  }
180
181
  }
@@ -248,6 +249,85 @@ function injectSystemTextIntoMessages(source, text) {
248
249
  messages.splice(insertAt, 0, sys);
249
250
  return messages;
250
251
  }
252
+ function buildStandardFollowupTools() {
253
+ // Keep this list minimal and stable. Used only as a best-effort fallback when a followup hop
254
+ // would otherwise have no tools at all (which can cause tool-based clients to "break" mid-session).
255
+ return [
256
+ {
257
+ type: 'function',
258
+ function: {
259
+ name: 'shell',
260
+ description: 'Runs a shell command and returns its output.',
261
+ parameters: {
262
+ type: 'object',
263
+ properties: {
264
+ command: { oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] },
265
+ workdir: { type: 'string' }
266
+ },
267
+ required: ['command'],
268
+ additionalProperties: false
269
+ }
270
+ }
271
+ },
272
+ {
273
+ type: 'function',
274
+ function: {
275
+ name: 'exec_command',
276
+ description: 'Execute a command in a PTY and return output.',
277
+ parameters: {
278
+ type: 'object',
279
+ properties: {
280
+ cmd: { type: 'string' },
281
+ workdir: { type: 'string' },
282
+ timeout_ms: { type: 'number' },
283
+ max_output_tokens: { type: 'number' },
284
+ yield_time_ms: { type: 'number' }
285
+ },
286
+ required: ['cmd'],
287
+ additionalProperties: false
288
+ }
289
+ }
290
+ },
291
+ {
292
+ type: 'function',
293
+ function: {
294
+ name: 'apply_patch',
295
+ description: 'Apply a patch to repository files.',
296
+ parameters: {
297
+ type: 'object',
298
+ properties: {
299
+ patch: { type: 'string' }
300
+ },
301
+ required: ['patch'],
302
+ additionalProperties: false
303
+ }
304
+ }
305
+ }
306
+ ];
307
+ }
308
+ function ensureStandardToolsIfMissing(current) {
309
+ const existing = Array.isArray(current) ? cloneJson(current) : [];
310
+ const seen = new Set();
311
+ for (const tool of existing) {
312
+ if (!tool || typeof tool !== 'object' || Array.isArray(tool))
313
+ continue;
314
+ const fn = tool.function;
315
+ const name = fn && typeof fn === 'object' && typeof fn.name === 'string' ? String(fn.name).trim() : '';
316
+ if (name)
317
+ seen.add(name);
318
+ }
319
+ for (const tool of buildStandardFollowupTools()) {
320
+ const fn = tool.function;
321
+ const name = fn && typeof fn === 'object' && typeof fn.name === 'string' ? String(fn.name).trim() : '';
322
+ if (!name)
323
+ continue;
324
+ if (seen.has(name))
325
+ continue;
326
+ existing.push(tool);
327
+ seen.add(name);
328
+ }
329
+ return existing;
330
+ }
251
331
  /**
252
332
  * Build a canonical followup request body from injection ops.
253
333
  *
@@ -281,6 +361,10 @@ export function buildServerToolFollowupChatPayloadFromInjection(args) {
281
361
  // No-op: tools are preserved by default. Kept for backward compatibility.
282
362
  continue;
283
363
  }
364
+ if (op.op === 'ensure_standard_tools') {
365
+ tools = ensureStandardToolsIfMissing(tools);
366
+ continue;
367
+ }
284
368
  if (op.op === 'trim_openai_messages') {
285
369
  const maxNonSystemMessages = typeof op.maxNonSystemMessages === 'number'
286
370
  ? op.maxNonSystemMessages
@@ -178,7 +178,7 @@ const handler = async (ctx) => {
178
178
  injection: {
179
179
  ops: [
180
180
  { op: 'append_assistant_message', required: false },
181
- { op: 'preserve_tools' },
181
+ { op: 'ensure_standard_tools' },
182
182
  { op: 'append_user_text', text }
183
183
  ]
184
184
  },
@@ -2,6 +2,7 @@ import type { JsonObject } from '../conversion/hub/types/json.js';
2
2
  import type { ServerSideToolEngineOptions, ServerSideToolEngineResult, ToolCall } from './types.js';
3
3
  import './handlers/iflow-model-error-retry.js';
4
4
  import './handlers/gemini-empty-reply-continue.js';
5
+ import './handlers/antigravity-thought-signature-bootstrap.js';
5
6
  import './handlers/stop-message-auto.js';
6
7
  import './handlers/clock.js';
7
8
  import './handlers/clock-auto.js';
@@ -4,6 +4,7 @@ import { executeWebSearchBackendPlan } from './handlers/web-search.js';
4
4
  import { executeVisionBackendPlan } from './handlers/vision.js';
5
5
  import './handlers/iflow-model-error-retry.js';
6
6
  import './handlers/gemini-empty-reply-continue.js';
7
+ import './handlers/antigravity-thought-signature-bootstrap.js';
7
8
  import './handlers/stop-message-auto.js';
8
9
  import './handlers/clock.js';
9
10
  import './handlers/clock-auto.js';
@@ -51,6 +51,8 @@ export interface ServerSideToolEngineOptions {
51
51
  }
52
52
  export type ServerToolFollowupInjectionOp = {
53
53
  op: 'preserve_tools';
54
+ } | {
55
+ op: 'ensure_standard_tools';
54
56
  } | {
55
57
  op: 'append_assistant_message';
56
58
  required?: boolean;