@jsonstudio/llms 0.6.954 → 0.6.1172

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 (134) hide show
  1. package/dist/conversion/hub/operation-table/operation-table-runner.d.ts +18 -0
  2. package/dist/conversion/hub/operation-table/operation-table-runner.js +158 -0
  3. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.d.ts +8 -0
  4. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +303 -0
  5. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.d.ts +8 -0
  6. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +413 -0
  7. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.d.ts +7 -0
  8. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +841 -0
  9. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.d.ts +21 -0
  10. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +535 -0
  11. package/dist/conversion/hub/ops/operations.d.ts +19 -0
  12. package/dist/conversion/hub/ops/operations.js +126 -0
  13. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +9 -0
  14. package/dist/conversion/hub/pipeline/hub-pipeline.js +489 -19
  15. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +6 -0
  16. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +11 -0
  17. package/dist/conversion/hub/policy/policy-engine.js +41 -9
  18. package/dist/conversion/hub/policy/protocol-spec.d.ts +25 -0
  19. package/dist/conversion/hub/policy/protocol-spec.js +73 -23
  20. package/dist/conversion/hub/process/chat-process.js +252 -41
  21. package/dist/conversion/hub/response/provider-response.js +175 -2
  22. package/dist/conversion/hub/response/response-runtime.js +1 -1
  23. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.d.ts +1 -8
  24. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +1 -365
  25. package/dist/conversion/hub/semantic-mappers/chat-mapper.d.ts +1 -8
  26. package/dist/conversion/hub/semantic-mappers/chat-mapper.js +1 -467
  27. package/dist/conversion/hub/semantic-mappers/gemini-mapper.d.ts +1 -7
  28. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +1 -903
  29. package/dist/conversion/hub/semantic-mappers/responses-mapper.d.ts +1 -21
  30. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +1 -593
  31. package/dist/conversion/hub/tool-surface/tool-surface-engine.d.ts +18 -0
  32. package/dist/conversion/hub/tool-surface/tool-surface-engine.js +571 -0
  33. package/dist/conversion/responses/responses-openai-bridge.js +14 -2
  34. package/dist/conversion/shared/bridge-message-utils.js +2 -8
  35. package/dist/conversion/shared/bridge-policies.js +5 -105
  36. package/dist/conversion/shared/gemini-tool-utils.js +89 -15
  37. package/dist/conversion/shared/protocol-field-allowlists.d.ts +7 -0
  38. package/dist/conversion/shared/protocol-field-allowlists.js +145 -0
  39. package/dist/conversion/shared/reasoning-tool-normalizer.js +4 -2
  40. package/dist/conversion/shared/snapshot-hooks.js +166 -3
  41. package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
  42. package/dist/conversion/shared/text-markup-normalizer.js +345 -9
  43. package/dist/conversion/shared/thought-signature-validator.d.ts +52 -0
  44. package/dist/conversion/shared/thought-signature-validator.js +170 -0
  45. package/dist/conversion/shared/tool-argument-repairer.d.ts +39 -0
  46. package/dist/conversion/shared/tool-argument-repairer.js +56 -0
  47. package/dist/conversion/shared/tool-call-id-manager.d.ts +113 -0
  48. package/dist/conversion/shared/tool-call-id-manager.js +231 -0
  49. package/dist/conversion/shared/tool-canonicalizer.js +2 -11
  50. package/dist/router/virtual-router/bootstrap.js +70 -5
  51. package/dist/router/virtual-router/context-advisor.d.ts +4 -0
  52. package/dist/router/virtual-router/context-advisor.js +3 -0
  53. package/dist/router/virtual-router/context-weighted.d.ts +31 -0
  54. package/dist/router/virtual-router/context-weighted.js +54 -0
  55. package/dist/router/virtual-router/engine-selection.js +284 -47
  56. package/dist/router/virtual-router/engine.d.ts +3 -0
  57. package/dist/router/virtual-router/engine.js +142 -33
  58. package/dist/router/virtual-router/health-weighted.d.ts +25 -0
  59. package/dist/router/virtual-router/health-weighted.js +63 -0
  60. package/dist/router/virtual-router/load-balancer.d.ts +2 -0
  61. package/dist/router/virtual-router/load-balancer.js +45 -16
  62. package/dist/router/virtual-router/routing-instructions.js +17 -1
  63. package/dist/router/virtual-router/sticky-session-store.js +136 -24
  64. package/dist/router/virtual-router/stop-message-file-resolver.d.ts +1 -0
  65. package/dist/router/virtual-router/stop-message-file-resolver.js +74 -0
  66. package/dist/router/virtual-router/stop-message-state-sync.d.ts +15 -0
  67. package/dist/router/virtual-router/stop-message-state-sync.js +57 -0
  68. package/dist/router/virtual-router/types.d.ts +98 -0
  69. package/dist/servertool/clock/config.d.ts +7 -0
  70. package/dist/servertool/clock/config.js +27 -0
  71. package/dist/servertool/clock/daemon.d.ts +3 -0
  72. package/dist/servertool/clock/daemon.js +79 -0
  73. package/dist/servertool/clock/io.d.ts +2 -0
  74. package/dist/servertool/clock/io.js +13 -0
  75. package/dist/servertool/clock/paths.d.ts +4 -0
  76. package/dist/servertool/clock/paths.js +25 -0
  77. package/dist/servertool/clock/session-store.d.ts +3 -0
  78. package/dist/servertool/clock/session-store.js +56 -0
  79. package/dist/servertool/clock/state.d.ts +5 -0
  80. package/dist/servertool/clock/state.js +62 -0
  81. package/dist/servertool/clock/task-store.d.ts +5 -0
  82. package/dist/servertool/clock/task-store.js +4 -0
  83. package/dist/servertool/clock/tasks.d.ts +17 -0
  84. package/dist/servertool/clock/tasks.js +221 -0
  85. package/dist/servertool/clock/types.d.ts +36 -0
  86. package/dist/servertool/clock/types.js +1 -0
  87. package/dist/servertool/engine.d.ts +2 -0
  88. package/dist/servertool/engine.js +161 -7
  89. package/dist/servertool/followup-shadow.d.ts +16 -0
  90. package/dist/servertool/followup-shadow.js +145 -0
  91. package/dist/servertool/handlers/apply-patch-guard.js +1 -265
  92. package/dist/servertool/handlers/clock-auto.d.ts +1 -0
  93. package/dist/servertool/handlers/clock-auto.js +160 -0
  94. package/dist/servertool/handlers/clock.d.ts +1 -0
  95. package/dist/servertool/handlers/clock.js +197 -0
  96. package/dist/servertool/handlers/exec-command-guard.js +7 -555
  97. package/dist/servertool/handlers/followup-request-builder.d.ts +15 -7
  98. package/dist/servertool/handlers/followup-request-builder.js +248 -28
  99. package/dist/servertool/handlers/gemini-empty-reply-continue.js +62 -169
  100. package/dist/servertool/handlers/iflow-model-error-retry.js +18 -28
  101. package/dist/servertool/handlers/recursive-detection-guard.d.ts +1 -0
  102. package/dist/servertool/handlers/recursive-detection-guard.js +333 -0
  103. package/dist/servertool/handlers/stop-message-auto.js +47 -175
  104. package/dist/servertool/handlers/vision.d.ts +7 -1
  105. package/dist/servertool/handlers/vision.js +61 -117
  106. package/dist/servertool/handlers/web-search.d.ts +7 -1
  107. package/dist/servertool/handlers/web-search.js +122 -105
  108. package/dist/servertool/reenter-backend.d.ts +23 -0
  109. package/dist/servertool/reenter-backend.js +18 -0
  110. package/dist/servertool/server-side-tools.d.ts +3 -2
  111. package/dist/servertool/server-side-tools.js +64 -10
  112. package/dist/servertool/types.d.ts +92 -3
  113. package/dist/sse/json-to-sse/event-generators/responses.js +3 -21
  114. package/dist/sse/shared/serializers/responses-event-serializer.d.ts +8 -0
  115. package/dist/sse/shared/serializers/responses-event-serializer.js +19 -0
  116. package/dist/sse/shared/writer.js +24 -7
  117. package/dist/tools/apply-patch/execution-capturer.js +3 -1
  118. package/dist/tools/apply-patch/json/parse-loose.d.ts +3 -0
  119. package/dist/tools/apply-patch/json/parse-loose.js +139 -0
  120. package/dist/tools/apply-patch/patch-text/context-diff.d.ts +1 -0
  121. package/dist/tools/apply-patch/patch-text/context-diff.js +173 -0
  122. package/dist/tools/apply-patch/patch-text/git-diff.d.ts +1 -0
  123. package/dist/tools/apply-patch/patch-text/git-diff.js +138 -0
  124. package/dist/tools/apply-patch/patch-text/looks-like-patch.d.ts +1 -0
  125. package/dist/tools/apply-patch/patch-text/looks-like-patch.js +13 -0
  126. package/dist/tools/apply-patch/patch-text/normalize.d.ts +3 -0
  127. package/dist/tools/apply-patch/patch-text/normalize.js +262 -0
  128. package/dist/tools/apply-patch/structured/coercion.d.ts +3 -0
  129. package/dist/tools/apply-patch/structured/coercion.js +82 -0
  130. package/dist/tools/apply-patch/validation/shared.d.ts +3 -0
  131. package/dist/tools/apply-patch/validation/shared.js +6 -0
  132. package/dist/tools/apply-patch/validator.d.ts +2 -2
  133. package/dist/tools/apply-patch/validator.js +6 -556
  134. package/package.json +1 -1
@@ -0,0 +1,16 @@
1
+ import type { StageRecorder } from '../conversion/hub/format-adapters/index.js';
2
+ import type { JsonObject } from '../conversion/hub/types/json.js';
3
+ export type HubFollowupMode = 'off' | 'shadow' | 'enforce';
4
+ export interface HubFollowupConfig {
5
+ mode: HubFollowupMode;
6
+ sampleRate?: number;
7
+ }
8
+ export declare function resolveHubFollowupConfigFromEnv(): HubFollowupConfig;
9
+ export declare function applyHubFollowupPolicyShadow(args: {
10
+ config?: HubFollowupConfig;
11
+ requestId?: string;
12
+ entryEndpoint?: string;
13
+ flowId?: string;
14
+ payload: JsonObject;
15
+ stageRecorder?: StageRecorder;
16
+ }): JsonObject;
@@ -0,0 +1,145 @@
1
+ import { isJsonObject, jsonClone } from '../conversion/hub/types/json.js';
2
+ function clampSampleRate(value) {
3
+ const num = typeof value === 'number' && Number.isFinite(value) ? value : 1;
4
+ if (num <= 0)
5
+ return 0;
6
+ if (num >= 1)
7
+ return 1;
8
+ return num;
9
+ }
10
+ function fnv1a32(input) {
11
+ let hash = 0x811c9dc5;
12
+ for (let i = 0; i < input.length; i++) {
13
+ hash ^= input.charCodeAt(i);
14
+ hash = (hash * 0x01000193) >>> 0;
15
+ }
16
+ return hash >>> 0;
17
+ }
18
+ function shouldSample(config, requestId) {
19
+ const rate = clampSampleRate(config.sampleRate);
20
+ if (rate <= 0)
21
+ return false;
22
+ if (rate >= 1)
23
+ return true;
24
+ const key = typeof requestId === 'string' && requestId.trim().length ? requestId.trim() : 'no_request_id';
25
+ const bucket = fnv1a32(key) / 0xffffffff;
26
+ return bucket < rate;
27
+ }
28
+ function readMode(raw) {
29
+ const normalized = raw.trim().toLowerCase();
30
+ if (!normalized)
31
+ return null;
32
+ if (normalized === 'off' || normalized === '0' || normalized === 'false')
33
+ return 'off';
34
+ if (normalized === 'shadow')
35
+ return 'shadow';
36
+ if (normalized === 'enforce')
37
+ return 'enforce';
38
+ return null;
39
+ }
40
+ export function resolveHubFollowupConfigFromEnv() {
41
+ const raw = String(process.env.ROUTECODEX_HUB_FOLLOWUP_MODE || '').trim();
42
+ const mode = readMode(raw) ?? 'off';
43
+ const sampleRateRaw = String(process.env.ROUTECODEX_HUB_FOLLOWUP_SAMPLE_RATE || '').trim();
44
+ const sampleRate = sampleRateRaw ? Number(sampleRateRaw) : undefined;
45
+ return {
46
+ mode,
47
+ ...(Number.isFinite(sampleRate) ? { sampleRate } : {})
48
+ };
49
+ }
50
+ function dropKeyByPrefix(root, prefixes) {
51
+ for (const key of Object.keys(root)) {
52
+ if (prefixes.some((p) => key.startsWith(p))) {
53
+ try {
54
+ delete root[key];
55
+ }
56
+ catch {
57
+ root[key] = undefined;
58
+ }
59
+ }
60
+ }
61
+ }
62
+ function normalizeFollowupPayload(payload) {
63
+ const out = jsonClone(payload);
64
+ const record = out;
65
+ // Followup requests must be non-streaming and must not carry route hints.
66
+ if (record.stream !== undefined) {
67
+ record.stream = false;
68
+ }
69
+ if (record.routeHint !== undefined) {
70
+ delete record.routeHint;
71
+ }
72
+ if (record.route_hint !== undefined) {
73
+ delete record.route_hint;
74
+ }
75
+ // Remove internal/private carriers from the body (metadata belongs to request metadata, not body).
76
+ dropKeyByPrefix(record, ['__']);
77
+ const parameters = record.parameters;
78
+ if (isJsonObject(parameters)) {
79
+ const params = parameters;
80
+ if (params.stream !== undefined) {
81
+ delete params.stream;
82
+ }
83
+ }
84
+ return out;
85
+ }
86
+ function diffPayloads(baseline, candidate, p = '<root>') {
87
+ if (Object.is(baseline, candidate))
88
+ return [];
89
+ if (typeof baseline !== typeof candidate)
90
+ return [{ path: p, baseline, candidate }];
91
+ if (Array.isArray(baseline) && Array.isArray(candidate)) {
92
+ const max = Math.max(baseline.length, candidate.length);
93
+ const diffs = [];
94
+ for (let i = 0; i < max; i += 1) {
95
+ diffs.push(...diffPayloads(baseline[i], candidate[i], `${p}[${i}]`));
96
+ }
97
+ return diffs;
98
+ }
99
+ if (baseline && typeof baseline === 'object' && candidate && typeof candidate === 'object') {
100
+ const a = baseline;
101
+ const b = candidate;
102
+ const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
103
+ const diffs = [];
104
+ for (const key of keys) {
105
+ const next = p === '<root>' ? key : `${p}.${key}`;
106
+ if (!(key in b))
107
+ diffs.push({ path: next, baseline: a[key], candidate: undefined });
108
+ else if (!(key in a))
109
+ diffs.push({ path: next, baseline: undefined, candidate: b[key] });
110
+ else
111
+ diffs.push(...diffPayloads(a[key], b[key], next));
112
+ }
113
+ return diffs;
114
+ }
115
+ return [{ path: p, baseline, candidate }];
116
+ }
117
+ export function applyHubFollowupPolicyShadow(args) {
118
+ const cfg = args.config ?? resolveHubFollowupConfigFromEnv();
119
+ if (!cfg || cfg.mode === 'off') {
120
+ return args.payload;
121
+ }
122
+ if (!shouldSample(cfg, args.requestId)) {
123
+ return args.payload;
124
+ }
125
+ const candidate = normalizeFollowupPayload(args.payload);
126
+ const diffs = diffPayloads(args.payload, candidate);
127
+ if (diffs.length > 0) {
128
+ const stage = `hub_followup.${cfg.mode}.payload`;
129
+ args.stageRecorder?.record(stage, {
130
+ kind: 'hub_followup_payload_shadow',
131
+ requestId: args.requestId,
132
+ entryEndpoint: args.entryEndpoint,
133
+ flowId: args.flowId,
134
+ diffCount: diffs.length,
135
+ diffPaths: diffs.slice(0, 50).map((d) => d.path),
136
+ diffHead: diffs.slice(0, 50).map((d) => ({ path: d.path, baseline: d.baseline, candidate: d.candidate })),
137
+ baseline: jsonClone(args.payload),
138
+ candidate: jsonClone(candidate)
139
+ });
140
+ }
141
+ if (cfg.mode === 'enforce') {
142
+ return candidate;
143
+ }
144
+ return args.payload;
145
+ }
@@ -1,269 +1,5 @@
1
1
  import { registerServerToolHandler } from '../registry.js';
2
- import { cloneJson } from '../server-side-tools.js';
3
- import { validateToolCall } from '../../tools/tool-registry.js';
4
- import { buildEntryAwareFollowupPayload, extractCapturedChatSeed } from './followup-request-builder.js';
5
- const FLOW_ID = 'apply_patch_guard';
6
2
  const handler = async (ctx) => {
7
- const toolCall = ctx.toolCall;
8
- if (!toolCall) {
9
- return null;
10
- }
11
- if (!ctx.options.reenterPipeline) {
12
- return null;
13
- }
14
- const rawArgs = typeof toolCall.arguments === 'string' ? toolCall.arguments : '';
15
- const validation = validateToolCall('apply_patch', rawArgs);
16
- if (validation?.ok) {
17
- return null;
18
- }
19
- const reason = typeof validation?.reason === 'string' && validation.reason.trim()
20
- ? validation.reason.trim()
21
- : 'unknown';
22
- const snippet = rawArgs && rawArgs.trim().length
23
- ? rawArgs.trim().slice(0, 200).replace(/\s+/g, ' ')
24
- : '';
25
- const isExecCommandShape = extractLikelyExecCommandShape(rawArgs);
26
- // For obviously wrong shapes (e.g. exec_command-like payload passed into apply_patch),
27
- // do NOT attempt a followup "recovery" loop. Return a plain assistant message instead.
28
- if (isExecCommandShape && (reason === 'missing_changes' || reason === 'missing_payload' || reason === 'invalid_json')) {
29
- const patched = injectApplyPatchHardFailureMessage(ctx.base, toolCall, {
30
- reason,
31
- snippet,
32
- rawArgs
33
- });
34
- return {
35
- chatResponse: patched,
36
- execution: { flowId: FLOW_ID }
37
- };
38
- }
39
- const patched = injectApplyPatchRejectedToolResult(ctx.base, toolCall, {
40
- reason,
41
- snippet,
42
- rawArgs
43
- });
44
- const followupPayload = buildToolFollowupPayload(ctx.adapterContext, patched, ctx.options.entryEndpoint || ctx.adapterContext?.entryEndpoint || '/v1/chat/completions');
45
- if (!followupPayload) {
46
- // Fail-closed: if we cannot perform reenter, do not intercept.
47
- // (This should be rare because HubPipeline always provides capturedChatRequest.)
48
- return null;
49
- }
50
- return {
51
- chatResponse: patched,
52
- execution: {
53
- flowId: FLOW_ID,
54
- followup: {
55
- requestIdSuffix: ':apply_patch_guard_followup',
56
- payload: followupPayload
57
- }
58
- }
59
- };
3
+ return null;
60
4
  };
61
5
  registerServerToolHandler('apply_patch', handler);
62
- function injectApplyPatchHardFailureMessage(base, toolCall, options) {
63
- const cloned = cloneJson(base);
64
- const choices = Array.isArray(cloned.choices)
65
- ? cloned.choices
66
- : [];
67
- if (!choices.length) {
68
- return cloned;
69
- }
70
- const first = choices[0];
71
- if (!first || typeof first !== 'object' || Array.isArray(first)) {
72
- return cloned;
73
- }
74
- const message = first.message;
75
- if (!message || typeof message !== 'object' || Array.isArray(message)) {
76
- return cloned;
77
- }
78
- // Replace tool-call response with a plain assistant message.
79
- const guidance = buildApplyPatchGuidance(options.rawArgs);
80
- const text = `[apply_patch rejected] reason=${options.reason}` +
81
- (options.snippet ? ` args=${options.snippet}` : '') +
82
- `\n\n${guidance}`;
83
- message.content = text;
84
- if (Object.prototype.hasOwnProperty.call(message, 'tool_calls')) {
85
- delete message.tool_calls;
86
- }
87
- if (typeof first.finish_reason === 'string' && first.finish_reason === 'tool_calls') {
88
- first.finish_reason = 'stop';
89
- }
90
- // Remove tool_outputs to avoid confusing clients that don't implement tool loops.
91
- if (Object.prototype.hasOwnProperty.call(cloned, 'tool_outputs')) {
92
- delete cloned.tool_outputs;
93
- }
94
- return cloned;
95
- }
96
- function injectApplyPatchRejectedToolResult(base, toolCall, options) {
97
- const cloned = cloneJson(base);
98
- const existingOutputs = Array.isArray(cloned.tool_outputs)
99
- ? cloned.tool_outputs
100
- : [];
101
- const payload = {
102
- ok: false,
103
- tool: 'apply_patch',
104
- reason: options.reason,
105
- ...(options.snippet ? { argsSnippet: options.snippet } : {}),
106
- guidance: buildApplyPatchGuidance(options.rawArgs)
107
- };
108
- cloned.tool_outputs = [
109
- ...existingOutputs,
110
- {
111
- tool_call_id: toolCall.id,
112
- name: 'apply_patch',
113
- content: JSON.stringify(payload)
114
- }
115
- ];
116
- return cloned;
117
- }
118
- function buildApplyPatchGuidance(rawArgs) {
119
- const base = 'apply_patch 参数预检失败。请改用统一 diff(*** Begin Patch ... *** End Patch),' +
120
- '或使用结构化 JSON:{ file, changes:[{ file, kind, target/anchor, lines/newText, ... }] }。';
121
- const schemaHint = extractLikelyExecCommandShape(rawArgs);
122
- if (schemaHint) {
123
- return (base +
124
- '\n\n' +
125
- '你传入的参数看起来像 exec_command(例如包含 "command"/"cmd" 字段),但工具名是 apply_patch。\n' +
126
- 'apply_patch 必须提供非空的 "changes" 数组;如果你需要执行命令请改用 exec_command(注意:禁止通过 shell 写文件)。');
127
- }
128
- // User-requested hint (directory deletion is not supported via delete_file).
129
- // Heuristic: detect delete_file with a directory-like target (trailing slash).
130
- const hintTarget = extractDeleteFileTarget(rawArgs);
131
- if (!hintTarget) {
132
- return base;
133
- }
134
- return (base +
135
- '\n\n' +
136
- `- {"changes":[{"kind":"delete_file","target":"${hintTarget}"}]} ❌ 仍会 invalid_file(因为是目录)\n\n` +
137
- '如果你真要删目录:请用 exec_command(例如 rm -rf tmp web-container-manager),或者用 apply_patch 逐个删目录下的文件(更安全、可审计)。');
138
- }
139
- function extractDeleteFileTarget(rawArgs) {
140
- if (!rawArgs || typeof rawArgs !== 'string') {
141
- return null;
142
- }
143
- let parsed;
144
- try {
145
- parsed = JSON.parse(rawArgs);
146
- }
147
- catch {
148
- return null;
149
- }
150
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
151
- return null;
152
- }
153
- const record = parsed;
154
- const changes = Array.isArray(record.changes) ? record.changes : [];
155
- for (const change of changes) {
156
- if (!change || typeof change !== 'object' || Array.isArray(change))
157
- continue;
158
- const c = change;
159
- const kind = typeof c.kind === 'string' ? c.kind.trim().toLowerCase() : '';
160
- if (kind !== 'delete_file')
161
- continue;
162
- const target = typeof c.target === 'string' ? c.target.trim() : '';
163
- if (!target)
164
- continue;
165
- if (target.endsWith('/')) {
166
- return target;
167
- }
168
- }
169
- return null;
170
- }
171
- function extractLikelyExecCommandShape(rawArgs) {
172
- if (!rawArgs || typeof rawArgs !== 'string') {
173
- return false;
174
- }
175
- let parsed;
176
- try {
177
- parsed = JSON.parse(rawArgs);
178
- }
179
- catch {
180
- return false;
181
- }
182
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
183
- return false;
184
- }
185
- const record = parsed;
186
- const command = record.command;
187
- const cmd = record.cmd;
188
- if (typeof command === 'string' && command.trim()) {
189
- return true;
190
- }
191
- if (typeof cmd === 'string' && cmd.trim()) {
192
- return true;
193
- }
194
- return false;
195
- }
196
- function buildToolFollowupPayload(adapterContext, chatResponse, entryEndpoint) {
197
- const captured = adapterContext && typeof adapterContext === 'object'
198
- ? adapterContext.capturedChatRequest
199
- : undefined;
200
- const seed = extractCapturedChatSeed(captured);
201
- if (!seed) {
202
- return null;
203
- }
204
- const assistantMessage = extractAssistantMessage(chatResponse);
205
- if (!assistantMessage) {
206
- return null;
207
- }
208
- const toolMessages = buildToolMessages(chatResponse);
209
- if (!toolMessages.length) {
210
- return null;
211
- }
212
- const reconstructed = [...seed.messages, assistantMessage, ...toolMessages];
213
- return buildEntryAwareFollowupPayload({
214
- entryEndpoint,
215
- model: seed.model,
216
- messages: reconstructed,
217
- ...(seed.tools ? { tools: seed.tools } : {}),
218
- ...(seed.parameters ? { parameters: seed.parameters } : {})
219
- });
220
- }
221
- function extractAssistantMessage(chatResponse) {
222
- const choices = Array.isArray(chatResponse.choices)
223
- ? chatResponse.choices
224
- : [];
225
- if (!choices.length)
226
- return null;
227
- const first = choices[0];
228
- if (!first || typeof first !== 'object' || Array.isArray(first))
229
- return null;
230
- const message = first.message;
231
- if (!message || typeof message !== 'object' || Array.isArray(message))
232
- return null;
233
- return cloneJson(message);
234
- }
235
- function buildToolMessages(chatResponse) {
236
- const toolOutputs = Array.isArray(chatResponse.tool_outputs)
237
- ? chatResponse.tool_outputs
238
- : [];
239
- const messages = [];
240
- for (const entry of toolOutputs) {
241
- if (!entry || typeof entry !== 'object' || Array.isArray(entry))
242
- continue;
243
- const record = entry;
244
- const toolCallId = typeof record.tool_call_id === 'string' ? record.tool_call_id : undefined;
245
- if (!toolCallId)
246
- continue;
247
- const name = typeof record.name === 'string' && record.name.trim() ? record.name.trim() : 'apply_patch';
248
- const rawContent = record.content;
249
- let contentText;
250
- if (typeof rawContent === 'string') {
251
- contentText = rawContent;
252
- }
253
- else {
254
- try {
255
- contentText = JSON.stringify(rawContent ?? {});
256
- }
257
- catch {
258
- contentText = String(rawContent ?? '');
259
- }
260
- }
261
- messages.push({
262
- role: 'tool',
263
- tool_call_id: toolCallId,
264
- name,
265
- content: contentText
266
- });
267
- }
268
- return messages;
269
- }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,160 @@
1
+ import { registerServerToolHandler } from '../registry.js';
2
+ import { extractCapturedChatSeed } from './followup-request-builder.js';
3
+ import { findNextUndeliveredDueAtMs, listClockTasks, normalizeClockConfig, startClockDaemonIfNeeded } from '../clock/task-store.js';
4
+ const FLOW_ID = 'clock_hold_flow';
5
+ function resolveClientConnectionState(value) {
6
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
7
+ return null;
8
+ }
9
+ const record = value;
10
+ if (typeof record.disconnected !== 'boolean') {
11
+ return null;
12
+ }
13
+ return { disconnected: record.disconnected };
14
+ }
15
+ function isStopFinishReason(base) {
16
+ if (!base || typeof base !== 'object' || Array.isArray(base)) {
17
+ return false;
18
+ }
19
+ const payload = base;
20
+ const choicesRaw = payload.choices;
21
+ if (Array.isArray(choicesRaw) && choicesRaw.length) {
22
+ const first = choicesRaw[0];
23
+ if (!first || typeof first !== 'object' || Array.isArray(first)) {
24
+ return false;
25
+ }
26
+ const finishReasonRaw = first.finish_reason;
27
+ const finishReason = typeof finishReasonRaw === 'string' && finishReasonRaw.trim()
28
+ ? finishReasonRaw.trim().toLowerCase()
29
+ : '';
30
+ if (!finishReason || finishReason === 'tool_calls') {
31
+ return false;
32
+ }
33
+ if (finishReason !== 'stop' && finishReason !== 'length') {
34
+ return false;
35
+ }
36
+ const message = first.message &&
37
+ typeof first.message === 'object' &&
38
+ !Array.isArray(first.message)
39
+ ? first.message
40
+ : null;
41
+ if (!message) {
42
+ return false;
43
+ }
44
+ const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
45
+ if (toolCalls.length > 0) {
46
+ return false;
47
+ }
48
+ return true;
49
+ }
50
+ const statusRaw = typeof payload.status === 'string' ? payload.status.trim().toLowerCase() : '';
51
+ if (statusRaw && statusRaw !== 'completed') {
52
+ return false;
53
+ }
54
+ if (payload.required_action && typeof payload.required_action === 'object') {
55
+ return false;
56
+ }
57
+ return true;
58
+ }
59
+ function resolveSessionId(adapterContext) {
60
+ if (!adapterContext || typeof adapterContext !== 'object' || Array.isArray(adapterContext)) {
61
+ return null;
62
+ }
63
+ const sessionId = typeof adapterContext.sessionId === 'string' ? String(adapterContext.sessionId).trim() : '';
64
+ return sessionId || null;
65
+ }
66
+ function computeHoldSleepMs(remainingMs) {
67
+ if (remainingMs <= 0)
68
+ return 0;
69
+ if (remainingMs > 10 * 60_000)
70
+ return 30_000;
71
+ if (remainingMs > 60_000)
72
+ return 10_000;
73
+ if (remainingMs > 10_000)
74
+ return 1_000;
75
+ return 200;
76
+ }
77
+ async function sleep(ms) {
78
+ if (!Number.isFinite(ms) || ms <= 0) {
79
+ return;
80
+ }
81
+ await new Promise((resolve) => setTimeout(resolve, ms));
82
+ }
83
+ const handler = async (ctx) => {
84
+ const record = ctx.adapterContext;
85
+ // Only trigger on stop/length completion (no tool calls).
86
+ if (!isStopFinishReason(ctx.base)) {
87
+ return null;
88
+ }
89
+ // When client already disconnected, skip holding.
90
+ const connectionState = resolveClientConnectionState(record.clientConnectionState);
91
+ if (connectionState?.disconnected === true) {
92
+ return null;
93
+ }
94
+ const clientDisconnectedRaw = record.clientDisconnected;
95
+ if (clientDisconnectedRaw === true ||
96
+ (typeof clientDisconnectedRaw === 'string' && clientDisconnectedRaw.trim().toLowerCase() === 'true')) {
97
+ return null;
98
+ }
99
+ const clockConfig = normalizeClockConfig(record.clock);
100
+ if (!clockConfig) {
101
+ return null;
102
+ }
103
+ await startClockDaemonIfNeeded(clockConfig);
104
+ const sessionId = resolveSessionId(ctx.adapterContext);
105
+ if (!sessionId) {
106
+ return null;
107
+ }
108
+ const seed = extractCapturedChatSeed(record.capturedChatRequest);
109
+ if (!seed) {
110
+ return null;
111
+ }
112
+ const tasks = await listClockTasks(sessionId, clockConfig);
113
+ const at = Date.now();
114
+ const nextDueAtMs = findNextUndeliveredDueAtMs(tasks, at);
115
+ if (!nextDueAtMs) {
116
+ return null;
117
+ }
118
+ // Wait until the "due window" is reached (now >= dueAt - dueWindowMs).
119
+ const thresholdMs = nextDueAtMs - clockConfig.dueWindowMs;
120
+ while (Date.now() < thresholdMs) {
121
+ const state = resolveClientConnectionState(ctx.adapterContext.clientConnectionState);
122
+ if (state?.disconnected === true) {
123
+ return null;
124
+ }
125
+ const remaining = thresholdMs - Date.now();
126
+ await sleep(computeHoldSleepMs(remaining));
127
+ // Best-effort: if tasks were cleared/cancelled while holding, stop holding.
128
+ try {
129
+ const refreshed = await listClockTasks(sessionId, clockConfig);
130
+ const refreshedNext = findNextUndeliveredDueAtMs(refreshed, Date.now());
131
+ if (!refreshedNext) {
132
+ return null;
133
+ }
134
+ }
135
+ catch {
136
+ // ignore refresh errors; keep holding
137
+ }
138
+ }
139
+ return {
140
+ flowId: FLOW_ID,
141
+ finalize: async () => ({
142
+ chatResponse: ctx.base,
143
+ execution: {
144
+ flowId: FLOW_ID,
145
+ followup: {
146
+ requestIdSuffix: ':clock_hold_followup',
147
+ entryEndpoint: ctx.entryEndpoint,
148
+ injection: {
149
+ ops: [
150
+ { op: 'append_assistant_message', required: false },
151
+ { op: 'append_user_text', text: 'continue' }
152
+ ]
153
+ },
154
+ metadata: (connectionState ? { clientConnectionState: connectionState } : {})
155
+ }
156
+ }
157
+ })
158
+ };
159
+ };
160
+ registerServerToolHandler('clock_auto', handler, { trigger: 'auto' });
@@ -0,0 +1 @@
1
+ export {};