@flutchai/flutch-sdk 0.1.23 → 0.1.25

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/index.d.cts CHANGED
@@ -635,7 +635,6 @@ declare class CallbackStore {
635
635
  private readonly redis;
636
636
  private readonly isProduction;
637
637
  constructor(redis: Redis);
638
- private generateToken;
639
638
  issue(entry: CallbackEntry): Promise<string>;
640
639
  getAndLock(token: string): Promise<CallbackRecord | null>;
641
640
  private getAndLockAtomic;
@@ -1125,6 +1124,7 @@ interface ChannelState {
1125
1124
  contentChain: IContentBlock[];
1126
1125
  currentBlock: IContentBlock | null;
1127
1126
  pendingToolBlocks: IContentBlock[];
1127
+ toolBlocksByRunId: Map<string, IContentBlock>;
1128
1128
  }
1129
1129
  interface StreamAccumulator {
1130
1130
  channels: Map<StreamChannel, ChannelState>;
package/dist/index.d.ts CHANGED
@@ -635,7 +635,6 @@ declare class CallbackStore {
635
635
  private readonly redis;
636
636
  private readonly isProduction;
637
637
  constructor(redis: Redis);
638
- private generateToken;
639
638
  issue(entry: CallbackEntry): Promise<string>;
640
639
  getAndLock(token: string): Promise<CallbackRecord | null>;
641
640
  private getAndLockAtomic;
@@ -1125,6 +1124,7 @@ interface ChannelState {
1125
1124
  contentChain: IContentBlock[];
1126
1125
  currentBlock: IContentBlock | null;
1127
1126
  pendingToolBlocks: IContentBlock[];
1127
+ toolBlocksByRunId: Map<string, IContentBlock>;
1128
1128
  }
1129
1129
  interface StreamAccumulator {
1130
1130
  channels: Map<StreamChannel, ChannelState>;
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { ApiOperation, ApiResponse, ApiTags, ApiParam, DocumentBuilder, SwaggerM
4
4
  import * as fs from 'fs';
5
5
  import * as path2 from 'path';
6
6
  import * as os from 'os';
7
- import { randomBytes, createHash, randomUUID } from 'crypto';
7
+ import { createHash, randomUUID, randomBytes } from 'crypto';
8
8
  import { Registry, collectDefaultMetrics, Counter, Histogram, Gauge } from 'prom-client';
9
9
  import { NestFactory, MetadataScanner, ModuleRef, DiscoveryModule } from '@nestjs/core';
10
10
  import * as net from 'net';
@@ -1216,28 +1216,54 @@ var init_versioned_graph_service = __esm({
1216
1216
  ], VersionedGraphService);
1217
1217
  }
1218
1218
  });
1219
+ function generateCallbackToken(graphType) {
1220
+ return `cb::${graphType}::${randomBytes(8).toString("base64url")}`;
1221
+ }
1222
+ function createCallbackRecord(entry, token, now) {
1223
+ return {
1224
+ ...entry,
1225
+ token,
1226
+ status: "pending",
1227
+ createdAt: now,
1228
+ retries: 0
1229
+ };
1230
+ }
1231
+ function resolveCallbackTTL(entry) {
1232
+ return entry.metadata?.ttlSec ?? 600;
1233
+ }
1234
+ function parseCallbackRecord(data) {
1235
+ try {
1236
+ return JSON.parse(data);
1237
+ } catch {
1238
+ return null;
1239
+ }
1240
+ }
1241
+ function markAsFailed(record, error) {
1242
+ return {
1243
+ ...record,
1244
+ status: "failed",
1245
+ retries: (record.retries || 0) + 1,
1246
+ lastError: error
1247
+ };
1248
+ }
1249
+ function markAsPending(record) {
1250
+ return { ...record, status: "pending" };
1251
+ }
1252
+
1253
+ // src/callbacks/callback-store.ts
1219
1254
  var CallbackStore = class {
1220
1255
  constructor(redis) {
1221
1256
  this.redis = redis;
1222
1257
  this.isProduction = process.env.NODE_ENV === "production";
1223
1258
  }
1224
1259
  isProduction;
1225
- generateToken(graphType) {
1226
- return `cb::${graphType}::${randomBytes(8).toString("base64url")}`;
1227
- }
1228
1260
  /**
1229
1261
  * Issues a new callback token and persists its payload.
1230
1262
  */
1231
1263
  async issue(entry) {
1232
- const token = this.generateToken(entry.graphType);
1233
- const record = {
1234
- ...entry,
1235
- token,
1236
- status: "pending",
1237
- createdAt: Date.now(),
1238
- retries: 0
1239
- };
1240
- const ttl = entry.metadata?.ttlSec ?? 600;
1264
+ const token = generateCallbackToken(entry.graphType);
1265
+ const record = createCallbackRecord(entry, token, Date.now());
1266
+ const ttl = resolveCallbackTTL(entry);
1241
1267
  await this.redis.setex(`callback:${token}`, ttl, JSON.stringify(record));
1242
1268
  return token;
1243
1269
  }
@@ -1252,7 +1278,9 @@ var CallbackStore = class {
1252
1278
  }
1253
1279
  }
1254
1280
  /**
1255
- * Production version with Lua script for atomicity
1281
+ * Production version: uses Redis Lua scripting for atomic get-and-lock.
1282
+ * NOTE: redis.eval() here executes a Lua script on the Redis server,
1283
+ * NOT JavaScript eval(). This is the standard ioredis API for Lua scripting.
1256
1284
  */
1257
1285
  async getAndLockAtomic(token) {
1258
1286
  const script = `
@@ -1276,18 +1304,17 @@ var CallbackStore = class {
1276
1304
  if (!data) {
1277
1305
  return null;
1278
1306
  }
1279
- try {
1280
- const record = JSON.parse(data);
1281
- if (record.status !== "pending") {
1282
- return null;
1283
- }
1284
- record.status = "processing";
1285
- await this.redis.set(key, JSON.stringify(record));
1286
- return record;
1287
- } catch (error) {
1288
- console.error("Failed to parse callback record:", error);
1307
+ const record = parseCallbackRecord(data);
1308
+ if (!record) {
1309
+ console.error("Failed to parse callback record");
1310
+ return null;
1311
+ }
1312
+ if (record.status !== "pending") {
1289
1313
  return null;
1290
1314
  }
1315
+ record.status = "processing";
1316
+ await this.redis.set(key, JSON.stringify(record));
1317
+ return record;
1291
1318
  }
1292
1319
  /**
1293
1320
  * Finalizes callback processing by removing token.
@@ -1306,7 +1333,8 @@ var CallbackStore = class {
1306
1333
  }
1307
1334
  }
1308
1335
  /**
1309
- * Production version with Lua script for atomicity
1336
+ * Production version: uses Redis Lua scripting for atomic fail.
1337
+ * NOTE: redis.eval() here executes a Lua script on the Redis server.
1310
1338
  */
1311
1339
  async failAtomic(token, error) {
1312
1340
  const script = `
@@ -1331,17 +1359,14 @@ var CallbackStore = class {
1331
1359
  if (!data) {
1332
1360
  return null;
1333
1361
  }
1334
- try {
1335
- const record = JSON.parse(data);
1336
- record.status = "failed";
1337
- record.retries = (record.retries || 0) + 1;
1338
- record.lastError = error;
1339
- await this.redis.set(key, JSON.stringify(record));
1340
- return record;
1341
- } catch (parseError) {
1342
- console.error("Failed to parse callback record:", parseError);
1362
+ const record = parseCallbackRecord(data);
1363
+ if (!record) {
1364
+ console.error("Failed to parse callback record");
1343
1365
  return null;
1344
1366
  }
1367
+ const updated = markAsFailed(record, error);
1368
+ await this.redis.set(key, JSON.stringify(updated));
1369
+ return updated;
1345
1370
  }
1346
1371
  /**
1347
1372
  * Reset callback status to pending for retry.
@@ -1354,7 +1379,8 @@ var CallbackStore = class {
1354
1379
  }
1355
1380
  }
1356
1381
  /**
1357
- * Production version with Lua script for atomicity
1382
+ * Production version: uses Redis Lua scripting for atomic retry.
1383
+ * NOTE: redis.eval() here executes a Lua script on the Redis server.
1358
1384
  */
1359
1385
  async retryAtomic(token) {
1360
1386
  const script = `
@@ -1377,15 +1403,14 @@ var CallbackStore = class {
1377
1403
  if (!data) {
1378
1404
  return null;
1379
1405
  }
1380
- try {
1381
- const record = JSON.parse(data);
1382
- record.status = "pending";
1383
- await this.redis.set(key, JSON.stringify(record));
1384
- return record;
1385
- } catch (parseError) {
1386
- console.error("Failed to parse callback record:", parseError);
1406
+ const record = parseCallbackRecord(data);
1407
+ if (!record) {
1408
+ console.error("Failed to parse callback record");
1387
1409
  return null;
1388
1410
  }
1411
+ const updated = markAsPending(record);
1412
+ await this.redis.set(key, JSON.stringify(updated));
1413
+ return updated;
1389
1414
  }
1390
1415
  };
1391
1416
 
@@ -3037,6 +3062,38 @@ CallbackController = __decorateClass([
3037
3062
 
3038
3063
  // src/graph/abstract-graph.builder.ts
3039
3064
  init_agent_ui();
3065
+
3066
+ // src/graph/graph.logic.ts
3067
+ function isValidSemver(version) {
3068
+ return /^\d+\.\d+\.\d+$/.test(version);
3069
+ }
3070
+ function parseCallbackToken(token) {
3071
+ const parts = token.split("_");
3072
+ if (parts.length < 4 || parts[0] !== "cb") {
3073
+ return null;
3074
+ }
3075
+ const graphName = parts[1];
3076
+ const handler = parts[2];
3077
+ const graphType = `${graphName}::1.0.0`;
3078
+ return { graphType, handler };
3079
+ }
3080
+ function decodeCallbackParams(token) {
3081
+ const parts = token.split("_");
3082
+ if (parts.length < 4) {
3083
+ return {};
3084
+ }
3085
+ try {
3086
+ const encodedParams = parts.slice(3).join("_");
3087
+ const decodedParams = Buffer.from(encodedParams, "base64url").toString(
3088
+ "utf8"
3089
+ );
3090
+ return JSON.parse(decodedParams);
3091
+ } catch {
3092
+ return {};
3093
+ }
3094
+ }
3095
+
3096
+ // src/graph/abstract-graph.builder.ts
3040
3097
  var _AbstractGraphBuilder = class _AbstractGraphBuilder {
3041
3098
  logger = new Logger(_AbstractGraphBuilder.name);
3042
3099
  callbackRegistry;
@@ -3245,8 +3302,7 @@ var _AbstractGraphBuilder = class _AbstractGraphBuilder {
3245
3302
  * Version validation
3246
3303
  */
3247
3304
  validateVersion() {
3248
- const versionRegex = /^\d+\.\d+\.\d+$/;
3249
- if (!versionRegex.test(this.version)) {
3305
+ if (!isValidSemver(this.version)) {
3250
3306
  throw new Error(
3251
3307
  `Invalid version format: ${this.version}. Expected format: X.Y.Z`
3252
3308
  );
@@ -3596,36 +3652,21 @@ var UniversalGraphService = class {
3596
3652
  * Expected format: cb_{graphName}_{handler}_{encodedParams}
3597
3653
  */
3598
3654
  parseCallbackToken(token) {
3599
- const parts = token.split("_");
3600
- if (parts.length < 4 || parts[0] !== "cb") {
3655
+ const result = parseCallbackToken(token);
3656
+ if (!result) {
3601
3657
  throw new Error(`Invalid callback token format: ${token}`);
3602
3658
  }
3603
- const graphName = parts[1];
3604
- const handler = parts[2];
3605
- const graphType = `${graphName}::1.0.0`;
3606
- return { graphType, handler };
3659
+ return result;
3607
3660
  }
3608
3661
  /**
3609
3662
  * Extract parameters from callback token
3610
3663
  */
3611
3664
  parseCallbackParams(token) {
3612
- const parts = token.split("_");
3613
- if (parts.length < 4) {
3614
- return {};
3615
- }
3616
- try {
3617
- const encodedParams = parts.slice(3).join("_");
3618
- const decodedParams = Buffer.from(encodedParams, "base64url").toString(
3619
- "utf8"
3620
- );
3621
- return JSON.parse(decodedParams);
3622
- } catch (error) {
3623
- this.logger.warn(
3624
- `Failed to parse callback params from token: ${token}`,
3625
- error
3626
- );
3627
- return {};
3665
+ const result = decodeCallbackParams(token);
3666
+ if (Object.keys(result).length === 0 && token.split("_").length >= 4) {
3667
+ this.logger.warn(`Failed to parse callback params from token: ${token}`);
3628
3668
  }
3669
+ return result;
3629
3670
  }
3630
3671
  /**
3631
3672
  * Call a graph endpoint
@@ -4421,11 +4462,21 @@ var EventProcessor = class {
4421
4462
  channels: /* @__PURE__ */ new Map([
4422
4463
  [
4423
4464
  "text" /* TEXT */,
4424
- { contentChain: [], currentBlock: null, pendingToolBlocks: [] }
4465
+ {
4466
+ contentChain: [],
4467
+ currentBlock: null,
4468
+ pendingToolBlocks: [],
4469
+ toolBlocksByRunId: /* @__PURE__ */ new Map()
4470
+ }
4425
4471
  ],
4426
4472
  [
4427
4473
  "processing" /* PROCESSING */,
4428
- { contentChain: [], currentBlock: null, pendingToolBlocks: [] }
4474
+ {
4475
+ contentChain: [],
4476
+ currentBlock: null,
4477
+ pendingToolBlocks: [],
4478
+ toolBlocksByRunId: /* @__PURE__ */ new Map()
4479
+ }
4429
4480
  ]
4430
4481
  ]),
4431
4482
  attachments: [],
@@ -4598,11 +4649,20 @@ var EventProcessor = class {
4598
4649
  return;
4599
4650
  }
4600
4651
  if (event.event === "on_tool_start") {
4652
+ const channel = event.metadata?.stream_channel ?? "text" /* TEXT */;
4653
+ const state = acc.channels.get(channel);
4654
+ if (state && event.run_id) {
4655
+ const idx = state.pendingToolBlocks.findIndex(
4656
+ (b) => b.name === event.name
4657
+ );
4658
+ if (idx !== -1) {
4659
+ const block = state.pendingToolBlocks.splice(idx, 1)[0];
4660
+ state.toolBlocksByRunId.set(event.run_id, block);
4661
+ }
4662
+ }
4601
4663
  this.logger.log("\u{1F527} Tool execution started", {
4602
4664
  toolName: event.name,
4603
- input: event.data?.input,
4604
- runId: event.run_id,
4605
- metadata: event.metadata
4665
+ runId: event.run_id
4606
4666
  });
4607
4667
  return;
4608
4668
  }
@@ -4610,7 +4670,13 @@ var EventProcessor = class {
4610
4670
  const channel = event.metadata?.stream_channel ?? "text" /* TEXT */;
4611
4671
  const state = acc.channels.get(channel);
4612
4672
  if (!state) return;
4613
- const toolBlock = state.pendingToolBlocks.shift();
4673
+ let toolBlock;
4674
+ if (event.run_id && state.toolBlocksByRunId.has(event.run_id)) {
4675
+ toolBlock = state.toolBlocksByRunId.get(event.run_id);
4676
+ state.toolBlocksByRunId.delete(event.run_id);
4677
+ } else {
4678
+ toolBlock = state.pendingToolBlocks.shift();
4679
+ }
4614
4680
  if (toolBlock && toolBlock.type === "tool_use") {
4615
4681
  const output = event.data?.output;
4616
4682
  const outputString = typeof output === "string" ? output : JSON.stringify(output, null, 2);
@@ -4624,26 +4690,26 @@ var EventProcessor = class {
4624
4690
  },
4625
4691
  onPartial
4626
4692
  );
4627
- this.logger.log("\u2705 Tool execution completed", {
4693
+ this.logger.log("\u2705 Tool completed", {
4628
4694
  toolName: event.name,
4629
4695
  toolBlockId: toolBlock.id,
4630
- outputPreview: outputString.substring(0, 200) + (outputString.length > 200 ? "..." : ""),
4631
4696
  runId: event.run_id
4632
4697
  });
4633
4698
  } else {
4634
- this.logger.warn(
4635
- "\u26A0\uFE0F on_tool_end received but no pending tool block found",
4636
- {
4637
- toolName: event.name,
4638
- runId: event.run_id,
4639
- pendingCount: state.pendingToolBlocks.length
4640
- }
4641
- );
4699
+ this.logger.warn("\u26A0\uFE0F on_tool_end: no matching tool block", {
4700
+ toolName: event.name,
4701
+ runId: event.run_id
4702
+ });
4642
4703
  }
4643
4704
  return;
4644
4705
  }
4645
4706
  if (event.event === "on_tool_error") {
4646
- this.logger.error("\u274C Tool execution failed", {
4707
+ const channel = event.metadata?.stream_channel ?? "text" /* TEXT */;
4708
+ const state = acc.channels.get(channel);
4709
+ if (state && event.run_id) {
4710
+ state.toolBlocksByRunId.delete(event.run_id);
4711
+ }
4712
+ this.logger.error("\u274C Tool failed", {
4647
4713
  toolName: event.name,
4648
4714
  error: event.data?.error,
4649
4715
  runId: event.run_id
@@ -4691,6 +4757,13 @@ var EventProcessor = class {
4691
4757
  getResult(acc) {
4692
4758
  const allChains = [];
4693
4759
  for (const [channel, state] of acc.channels.entries()) {
4760
+ if (state.pendingToolBlocks.length > 0 || state.toolBlocksByRunId.size > 0) {
4761
+ this.logger.warn("\u26A0\uFE0F Orphaned tool blocks detected at finalization", {
4762
+ channel,
4763
+ pendingCount: state.pendingToolBlocks.length,
4764
+ mappedCount: state.toolBlocksByRunId.size
4765
+ });
4766
+ }
4694
4767
  if (state.currentBlock) {
4695
4768
  state.contentChain.push(state.currentBlock);
4696
4769
  }
@@ -5923,6 +5996,24 @@ var ChatFeature = /* @__PURE__ */ ((ChatFeature2) => {
5923
5996
  ChatFeature2["JSON_MODE"] = "json_mode";
5924
5997
  return ChatFeature2;
5925
5998
  })(ChatFeature || {});
5999
+ function isReasoningModel(modelName) {
6000
+ return modelName.includes("gpt-5") || modelName.includes("gpt-o1") || modelName.includes("gpt-o2") || modelName.includes("gpt-o3") || modelName.includes("gpt-o4") || /^gpt-(5|6|7|8|9)/.test(modelName) || /^gpt-o[1-4]/.test(modelName);
6001
+ }
6002
+ function hashToolsConfig(toolsConfig) {
6003
+ const sorted = toolsConfig.map((t) => `${t.toolName}:${t.enabled}:${JSON.stringify(t.config || {})}`).sort().join("|");
6004
+ return createHash("md5").update(sorted).digest("hex").slice(0, 16);
6005
+ }
6006
+ function generateModelCacheKey(modelId, temperature, maxTokens, toolsConfig) {
6007
+ const parts = [
6008
+ modelId,
6009
+ temperature ?? "default",
6010
+ maxTokens ?? "default"
6011
+ ];
6012
+ if (toolsConfig && toolsConfig.length > 0) {
6013
+ parts.push(hashToolsConfig(toolsConfig));
6014
+ }
6015
+ return parts.join(":");
6016
+ }
5926
6017
  var VoyageAIRerank = class extends BaseDocumentCompressor {
5927
6018
  apiKey;
5928
6019
  model;
@@ -6218,8 +6309,7 @@ var ModelInitializer = class _ModelInitializer {
6218
6309
  * Uses MD5 hash to create short, unique identifier
6219
6310
  */
6220
6311
  hashToolsConfig(toolsConfig) {
6221
- const sorted = toolsConfig.map((t) => `${t.toolName}:${t.enabled}:${JSON.stringify(t.config || {})}`).sort().join("|");
6222
- return createHash("md5").update(sorted).digest("hex").slice(0, 16);
6312
+ return hashToolsConfig(toolsConfig);
6223
6313
  }
6224
6314
  /**
6225
6315
  * Generate cache key from ModelByIdConfig
@@ -6227,16 +6317,12 @@ var ModelInitializer = class _ModelInitializer {
6227
6317
  * Example: "model123:0.7:4096" or "model123:0.7:4096:a1b2c3d4e5f6g7h8"
6228
6318
  */
6229
6319
  generateModelCacheKey(config) {
6230
- const parts = [
6320
+ return generateModelCacheKey(
6231
6321
  config.modelId,
6232
- config.temperature ?? "default",
6233
- config.maxTokens ?? "default"
6234
- ];
6235
- if (config.toolsConfig && config.toolsConfig.length > 0) {
6236
- const toolsHash = this.hashToolsConfig(config.toolsConfig);
6237
- parts.push(toolsHash);
6238
- }
6239
- return parts.join(":");
6322
+ config.temperature,
6323
+ config.maxTokens,
6324
+ config.toolsConfig
6325
+ );
6240
6326
  }
6241
6327
  /**
6242
6328
  * TEMPORARY SOLUTION for compatibility with new OpenAI models
@@ -6253,20 +6339,10 @@ var ModelInitializer = class _ModelInitializer {
6253
6339
  * @returns true if model requires maxCompletionTokens and temperature = 1
6254
6340
  */
6255
6341
  requiresMaxCompletionTokens(modelName) {
6256
- const requiresNew = modelName.includes("gpt-5") || modelName.includes("gpt-o1") || modelName.includes("gpt-o2") || modelName.includes("gpt-o3") || modelName.includes("gpt-o4") || // Add other patterns as new models are released
6257
- /^gpt-(5|6|7|8|9)/.test(modelName) || /^gpt-o[1-4]/.test(modelName);
6342
+ const requiresNew = isReasoningModel(modelName);
6258
6343
  this.logger.debug(`Checking token parameter for model "${modelName}"`, {
6259
6344
  modelName,
6260
- requiresMaxCompletionTokens: requiresNew,
6261
- checks: {
6262
- includesGpt5: modelName.includes("gpt-5"),
6263
- includesO1: modelName.includes("gpt-o1"),
6264
- includesO2: modelName.includes("gpt-o2"),
6265
- includesO3: modelName.includes("gpt-o3"),
6266
- includesO4: modelName.includes("gpt-o4"),
6267
- regexGpt5Plus: /^gpt-(5|6|7|8|9)/.test(modelName),
6268
- regexO1to4: /^gpt-o[1-4]/.test(modelName)
6269
- }
6345
+ requiresMaxCompletionTokens: requiresNew
6270
6346
  });
6271
6347
  return requiresNew;
6272
6348
  }