@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.
- package/dist/conversion/codecs/gemini-openai-codec.js +6 -1
- package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.d.ts +4 -6
- package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.js +179 -41
- package/dist/conversion/compat/actions/antigravity-thought-signature-cache.js +73 -14
- package/dist/conversion/compat/actions/antigravity-thought-signature-prepare.js +165 -10
- package/dist/conversion/compat/actions/gemini-cli-request.js +72 -13
- package/dist/conversion/compat/antigravity-session-signature.d.ts +68 -1
- package/dist/conversion/compat/antigravity-session-signature.js +833 -21
- package/dist/conversion/compat/profiles/anthropic-claude-code.json +17 -0
- package/dist/conversion/compat/profiles/chat-gemini-cli.json +1 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +33 -8
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +17 -1
- package/dist/conversion/hub/pipeline/compat/compat-profile-store.js +12 -3
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +1 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +24 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +20 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +26 -1
- package/dist/conversion/hub/process/chat-process.js +300 -67
- package/dist/conversion/hub/response/provider-response.js +4 -3
- package/dist/conversion/shared/gemini-tool-utils.js +134 -9
- package/dist/conversion/shared/text-markup-normalizer.js +90 -1
- package/dist/conversion/shared/thought-signature-validator.d.ts +1 -1
- package/dist/conversion/shared/thought-signature-validator.js +2 -1
- package/dist/quota/apikey-reset.d.ts +17 -0
- package/dist/quota/apikey-reset.js +43 -0
- package/dist/quota/index.d.ts +2 -0
- package/dist/quota/index.js +1 -0
- package/dist/quota/quota-manager.d.ts +44 -0
- package/dist/quota/quota-manager.js +491 -0
- package/dist/quota/quota-state.d.ts +6 -0
- package/dist/quota/quota-state.js +167 -0
- package/dist/quota/types.d.ts +61 -0
- package/dist/quota/types.js +1 -0
- package/dist/router/virtual-router/bootstrap.js +103 -6
- package/dist/router/virtual-router/engine-health.js +104 -0
- package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +18 -0
- package/dist/router/virtual-router/engine-selection/tier-priority.d.ts +1 -2
- package/dist/router/virtual-router/engine-selection/tier-priority.js +2 -2
- package/dist/router/virtual-router/engine-selection/tier-selection-select.js +34 -10
- package/dist/router/virtual-router/engine-selection/tier-selection.js +250 -6
- package/dist/router/virtual-router/engine-selection.js +2 -2
- package/dist/router/virtual-router/engine.d.ts +16 -1
- package/dist/router/virtual-router/engine.js +320 -42
- package/dist/router/virtual-router/features.js +20 -2
- package/dist/router/virtual-router/success-center.d.ts +10 -0
- package/dist/router/virtual-router/success-center.js +32 -0
- package/dist/router/virtual-router/types.d.ts +48 -0
- package/dist/servertool/clock/config.d.ts +2 -0
- package/dist/servertool/clock/config.js +10 -2
- package/dist/servertool/clock/daemon.js +3 -0
- package/dist/servertool/clock/ntp.d.ts +18 -0
- package/dist/servertool/clock/ntp.js +318 -0
- package/dist/servertool/clock/paths.d.ts +1 -0
- package/dist/servertool/clock/paths.js +3 -0
- package/dist/servertool/clock/state.d.ts +2 -0
- package/dist/servertool/clock/state.js +15 -2
- package/dist/servertool/clock/tasks.d.ts +1 -0
- package/dist/servertool/clock/tasks.js +24 -1
- package/dist/servertool/clock/types.d.ts +21 -0
- package/dist/servertool/engine.js +105 -1
- package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.d.ts +1 -0
- package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.js +201 -0
- package/dist/servertool/handlers/clock-auto.js +39 -4
- package/dist/servertool/handlers/clock.js +145 -16
- package/dist/servertool/handlers/followup-request-builder.js +84 -0
- package/dist/servertool/handlers/stop-message-auto.js +1 -1
- package/dist/servertool/server-side-tools.d.ts +1 -0
- package/dist/servertool/server-side-tools.js +1 -0
- package/dist/servertool/types.d.ts +2 -0
- package/dist/tools/apply-patch/execution-capturer.js +24 -3
- 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 =
|
|
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 (
|
|
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 -
|
|
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,
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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 (!
|
|
174
|
-
logClock('
|
|
266
|
+
if (!clockConfig) {
|
|
267
|
+
logClock('disabled', { action, hasSessionId: true });
|
|
175
268
|
return respond({
|
|
176
269
|
ok: false,
|
|
177
270
|
action,
|
|
178
|
-
message: 'clock
|
|
271
|
+
message: 'clock tool is not enabled (virtualrouter.clock.enabled=true required).'
|
|
179
272
|
});
|
|
180
273
|
}
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
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
|
|
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
|
|
@@ -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';
|