lemura 1.0.0 → 1.2.0

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.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var crypto = require('crypto');
4
+ var child_process = require('child_process');
4
5
 
5
6
  // src/types/errors.ts
6
7
  var LemuraError = class extends Error {
@@ -62,6 +63,24 @@ var LemuraToolTimeoutError = class extends LemuraError {
62
63
  this.name = "LemuraToolTimeoutError";
63
64
  }
64
65
  };
66
+ var LemuraMCPError = class extends LemuraError {
67
+ constructor(message, code = "MCP_ERROR", problem, hints = []) {
68
+ super(message, code, problem, hints);
69
+ this.name = "LemuraMCPError";
70
+ }
71
+ };
72
+ var LemuraMCPConnectionError = class extends LemuraMCPError {
73
+ constructor(message, problem, hints = []) {
74
+ super(message, "MCP_CONNECTION_FAILED", problem, hints);
75
+ this.name = "LemuraMCPConnectionError";
76
+ }
77
+ };
78
+ var LemuraMCPTimeoutError = class extends LemuraMCPError {
79
+ constructor(message) {
80
+ super(message, "MCP_TOOL_TIMEOUT");
81
+ this.name = "LemuraMCPTimeoutError";
82
+ }
83
+ };
65
84
 
66
85
  // src/types/logger.ts
67
86
  var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
@@ -76,7 +95,7 @@ var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
76
95
  // src/adapters/OpenAICompatibleAdapter.ts
77
96
  var OpenAICompatibleAdapter = class {
78
97
  name = "openai_compatible";
79
- version = "1.0.0";
98
+ version = "1.2.0";
80
99
  baseUrl;
81
100
  apiKey;
82
101
  defaultModel;
@@ -325,7 +344,7 @@ var OpenAICompatibleAdapter = class {
325
344
  const blob = new Blob([bytes], { type: request.mimeType });
326
345
  const formData = new FormData();
327
346
  formData.append("file", blob, "audio.webm");
328
- formData.append("model", "whisper-1");
347
+ formData.append("model", request.model || "whisper-1");
329
348
  if (request.language) formData.append("language", request.language);
330
349
  const response = await this.fetchWithRetry(`${this.baseUrl}/audio/transcriptions`, {
331
350
  method: "POST",
@@ -346,7 +365,7 @@ var OpenAICompatibleAdapter = class {
346
365
  const response = await this.fetchWithRetry(`${this.baseUrl}/audio/speech`, {
347
366
  method: "POST",
348
367
  body: JSON.stringify({
349
- model: "tts-1",
368
+ model: request.model || "tts-1",
350
369
  input: request.text,
351
370
  voice: request.voiceId || "alloy",
352
371
  response_format: request.format || "mp3"
@@ -369,7 +388,7 @@ var OpenAICompatibleAdapter = class {
369
388
  }
370
389
  async describeImage(request) {
371
390
  const payload = {
372
- model: this.defaultModel,
391
+ model: request.model || this.defaultModel,
373
392
  messages: [
374
393
  {
375
394
  role: "user",
@@ -401,7 +420,7 @@ var OpenAICompatibleAdapter = class {
401
420
  method: "POST",
402
421
  body: JSON.stringify({
403
422
  prompt: request.prompt,
404
- model: "dall-e-3",
423
+ model: request.model || "dall-e-3",
405
424
  n: 1,
406
425
  size: request.dimensions || "1024x1024"
407
426
  })
@@ -699,40 +718,336 @@ var InMemoryStorageAdapter = class {
699
718
  }
700
719
  };
701
720
 
721
+ // src/tools/SchemaValidator.ts
722
+ function typeOf(value) {
723
+ if (value === null) return "null";
724
+ if (Array.isArray(value)) return "array";
725
+ return typeof value;
726
+ }
727
+ function matchesType(value, requiredType) {
728
+ const t = typeOf(value);
729
+ if (requiredType === "integer") return t === "number" && Number.isInteger(value);
730
+ return t === requiredType;
731
+ }
732
+ function validateJsonSchema(value, schema, path = "") {
733
+ const errors = [];
734
+ if (schema["type"] !== void 0) {
735
+ const expectedType = schema["type"];
736
+ const types = Array.isArray(expectedType) ? expectedType : [expectedType];
737
+ if (!types.some((t) => matchesType(value, t))) {
738
+ errors.push({
739
+ path,
740
+ message: `Expected type '${types.join("|")}', got '${typeOf(value)}'`
741
+ });
742
+ return errors;
743
+ }
744
+ }
745
+ if (schema["enum"] !== void 0) {
746
+ const allowed = schema["enum"];
747
+ if (!allowed.some((a) => JSON.stringify(a) === JSON.stringify(value))) {
748
+ errors.push({ path, message: `Value must be one of: ${JSON.stringify(allowed)}` });
749
+ }
750
+ }
751
+ if ("const" in schema) {
752
+ if (JSON.stringify(schema["const"]) !== JSON.stringify(value)) {
753
+ errors.push({ path, message: `Value must equal ${JSON.stringify(schema["const"])}` });
754
+ }
755
+ }
756
+ if (typeof value === "string") {
757
+ if (schema["minLength"] !== void 0 && value.length < schema["minLength"]) {
758
+ errors.push({ path, message: `String too short (min ${schema["minLength"]}, got ${value.length})` });
759
+ }
760
+ if (schema["maxLength"] !== void 0 && value.length > schema["maxLength"]) {
761
+ errors.push({ path, message: `String too long (max ${schema["maxLength"]}, got ${value.length})` });
762
+ }
763
+ if (schema["pattern"] !== void 0) {
764
+ try {
765
+ const re = new RegExp(schema["pattern"]);
766
+ if (!re.test(value)) {
767
+ errors.push({ path, message: `String does not match pattern /${schema["pattern"]}/` });
768
+ }
769
+ } catch {
770
+ }
771
+ }
772
+ }
773
+ if (typeof value === "number") {
774
+ if (schema["minimum"] !== void 0 && value < schema["minimum"]) {
775
+ errors.push({ path, message: `Value ${value} is less than minimum ${schema["minimum"]}` });
776
+ }
777
+ if (schema["maximum"] !== void 0 && value > schema["maximum"]) {
778
+ errors.push({ path, message: `Value ${value} exceeds maximum ${schema["maximum"]}` });
779
+ }
780
+ if (schema["exclusiveMinimum"] !== void 0 && value <= schema["exclusiveMinimum"]) {
781
+ errors.push({ path, message: `Value ${value} must be > ${schema["exclusiveMinimum"]}` });
782
+ }
783
+ if (schema["exclusiveMaximum"] !== void 0 && value >= schema["exclusiveMaximum"]) {
784
+ errors.push({ path, message: `Value ${value} must be < ${schema["exclusiveMaximum"]}` });
785
+ }
786
+ }
787
+ if (Array.isArray(value)) {
788
+ if (schema["minItems"] !== void 0 && value.length < schema["minItems"]) {
789
+ errors.push({ path, message: `Array too short (min ${schema["minItems"]} items)` });
790
+ }
791
+ if (schema["maxItems"] !== void 0 && value.length > schema["maxItems"]) {
792
+ errors.push({ path, message: `Array too long (max ${schema["maxItems"]} items)` });
793
+ }
794
+ if (schema["items"] !== void 0) {
795
+ const itemSchema = schema["items"];
796
+ value.forEach((item, idx) => {
797
+ const childErrors = validateJsonSchema(item, itemSchema, `${path}[${idx}]`);
798
+ errors.push(...childErrors);
799
+ });
800
+ }
801
+ }
802
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
803
+ const obj = value;
804
+ const props = schema["properties"] || {};
805
+ const required = schema["required"] || [];
806
+ for (const key of required) {
807
+ if (!(key in obj)) {
808
+ errors.push({ path: path ? `${path}.${key}` : key, message: `Required property '${key}' is missing` });
809
+ }
810
+ }
811
+ for (const [key, propSchema] of Object.entries(props)) {
812
+ if (key in obj) {
813
+ const childErrors = validateJsonSchema(obj[key], propSchema, path ? `${path}.${key}` : key);
814
+ errors.push(...childErrors);
815
+ }
816
+ }
817
+ if (schema["additionalProperties"] === false) {
818
+ for (const key of Object.keys(obj)) {
819
+ if (!(key in props)) {
820
+ errors.push({ path: path ? `${path}.${key}` : key, message: `Additional property '${key}' is not allowed` });
821
+ }
822
+ }
823
+ } else if (schema["additionalProperties"] !== void 0 && typeof schema["additionalProperties"] === "object") {
824
+ const addlSchema = schema["additionalProperties"];
825
+ for (const [key, val] of Object.entries(obj)) {
826
+ if (!(key in props)) {
827
+ const childErrors = validateJsonSchema(val, addlSchema, path ? `${path}.${key}` : key);
828
+ errors.push(...childErrors);
829
+ }
830
+ }
831
+ }
832
+ if (schema["minProperties"] !== void 0 && Object.keys(obj).length < schema["minProperties"]) {
833
+ errors.push({ path, message: `Object has fewer than ${schema["minProperties"]} properties` });
834
+ }
835
+ if (schema["maxProperties"] !== void 0 && Object.keys(obj).length > schema["maxProperties"]) {
836
+ errors.push({ path, message: `Object has more than ${schema["maxProperties"]} properties` });
837
+ }
838
+ }
839
+ if (schema["allOf"] !== void 0) {
840
+ for (const subSchema of schema["allOf"]) {
841
+ errors.push(...validateJsonSchema(value, subSchema, path));
842
+ }
843
+ }
844
+ if (schema["anyOf"] !== void 0) {
845
+ const subSchemas = schema["anyOf"];
846
+ const anyPassed = subSchemas.some((s) => validateJsonSchema(value, s, path).length === 0);
847
+ if (!anyPassed) {
848
+ errors.push({ path, message: "Value does not match any of the anyOf schemas" });
849
+ }
850
+ }
851
+ if (schema["oneOf"] !== void 0) {
852
+ const subSchemas = schema["oneOf"];
853
+ const passing = subSchemas.filter((s) => validateJsonSchema(value, s, path).length === 0);
854
+ if (passing.length !== 1) {
855
+ errors.push({
856
+ path,
857
+ message: `Value must match exactly one of the oneOf schemas (matched ${passing.length})`
858
+ });
859
+ }
860
+ }
861
+ if (schema["not"] !== void 0) {
862
+ const subErrors = validateJsonSchema(value, schema["not"], path);
863
+ if (subErrors.length === 0) {
864
+ errors.push({ path, message: "Value must NOT match the not schema" });
865
+ }
866
+ }
867
+ return errors;
868
+ }
869
+
702
870
  // src/tools/ToolRegistry.ts
703
871
  var ToolRegistry = class {
704
872
  tools = /* @__PURE__ */ new Map();
705
- constructor(initialTools = []) {
873
+ defaultTimeoutMs;
874
+ constructor(initialTools = [], options = {}) {
875
+ this.defaultTimeoutMs = options.defaultTimeoutMs ?? 3e4;
706
876
  for (const tool of initialTools) {
707
877
  this.register(tool);
708
878
  }
709
879
  }
880
+ /**
881
+ * Registers a tool. Throws if a tool with the same name is already registered.
882
+ *
883
+ * @param tool - The tool definition to register
884
+ * @throws {LemuraError} If `tool.name` is already taken
885
+ */
710
886
  register(tool) {
711
887
  if (this.tools.has(tool.name)) {
712
- throw new Error(`Tool ${tool.name} is already registered.`);
888
+ throw new LemuraToolNotFoundError(`Tool '${tool.name}' is already registered. Use a unique name.`);
713
889
  }
714
890
  this.tools.set(tool.name, tool);
715
891
  }
892
+ /**
893
+ * Removes a registered tool by name.
894
+ *
895
+ * @param name - The tool name to unregister
896
+ */
897
+ unregister(name) {
898
+ return this.tools.delete(name);
899
+ }
900
+ /**
901
+ * Returns the tool definition for `name`, or `undefined` if not found.
902
+ *
903
+ * @param name - Tool name to look up
904
+ */
716
905
  get(name) {
717
906
  return this.tools.get(name);
718
907
  }
908
+ /**
909
+ * Returns all registered tool definitions.
910
+ */
719
911
  getAll() {
720
912
  return Array.from(this.tools.values());
721
913
  }
914
+ /**
915
+ * Executes a single tool call with schema validation and timeout enforcement.
916
+ *
917
+ * @param name - The tool name to execute
918
+ * @param params - The raw parameter object (validated against the tool's JSON Schema)
919
+ * @param context - Execution context (session, logger, adapters)
920
+ * @returns The tool's result
921
+ * @throws {LemuraToolNotFoundError} If the tool is not registered
922
+ * @throws {LemuraToolValidationError} If params fail JSON Schema validation
923
+ * @throws {LemuraToolTimeoutError} If execution exceeds the configured timeout
924
+ */
722
925
  async execute(name, params, context) {
723
926
  const tool = this.tools.get(name);
724
927
  if (!tool) {
725
- throw new LemuraToolNotFoundError(`Tool '${name}' not found.`);
928
+ throw new LemuraToolNotFoundError(`Tool '${name}' not found in registry.`);
929
+ }
930
+ if (tool.parameters && typeof tool.parameters === "object") {
931
+ const schemaErrors = validateJsonSchema(params, tool.parameters);
932
+ if (schemaErrors.length > 0) {
933
+ const msg = schemaErrors.map((e) => e.path ? `[${e.path}] ${e.message}` : e.message).join("; ");
934
+ throw new LemuraToolValidationError(
935
+ `Tool '${name}' parameter validation failed: ${msg}`
936
+ );
937
+ }
726
938
  }
939
+ const toolTimeoutMs = tool["timeoutMs"];
940
+ const timeoutMs = typeof toolTimeoutMs === "number" ? toolTimeoutMs : this.defaultTimeoutMs;
941
+ const executionPromise = tool.execute(params, context);
942
+ const timeoutPromise = new Promise((_, reject) => {
943
+ const id = setTimeout(() => {
944
+ clearTimeout(id);
945
+ reject(new LemuraToolTimeoutError(
946
+ `Tool '${name}' timed out after ${timeoutMs}ms`
947
+ ));
948
+ }, timeoutMs);
949
+ });
727
950
  try {
728
- const result = await tool.execute(params, context);
729
- return result;
951
+ return await Promise.race([executionPromise, timeoutPromise]);
730
952
  } catch (err) {
731
- if (err instanceof LemuraToolValidationError) {
953
+ if (err instanceof LemuraToolValidationError || err instanceof LemuraToolTimeoutError) {
732
954
  throw err;
733
955
  }
734
- throw new Error(`Tool execution failed for '${name}': ${err.message}`);
956
+ const message = err instanceof Error ? err.message : String(err);
957
+ throw new LemuraToolValidationError(
958
+ `Tool '${name}' execution failed: ${message}`
959
+ );
960
+ }
961
+ }
962
+ /**
963
+ * Executes multiple tool calls in parallel.
964
+ * All calls are issued simultaneously; results are returned in the same order as `calls`.
965
+ * Individual errors are captured per-call without aborting the others.
966
+ *
967
+ * @param calls - Array of `{ name, params }` objects
968
+ * @param context - Shared execution context
969
+ * @returns Array of `{ toolCallId, result?, error? }` in input order
970
+ *
971
+ * @example
972
+ * const results = await registry.executeParallel(
973
+ * [{ name: 'search_web', params: { query: 'foo' } }],
974
+ * context
975
+ * );
976
+ */
977
+ async executeParallel(calls, context) {
978
+ return Promise.all(
979
+ calls.map(async (call) => {
980
+ try {
981
+ const result = await this.execute(call.name, call.params, context);
982
+ return { id: call.id, result };
983
+ } catch (err) {
984
+ return { id: call.id, error: err instanceof Error ? err : new Error(String(err)) };
985
+ }
986
+ })
987
+ );
988
+ }
989
+ };
990
+
991
+ // src/tools/ToolFirewall.ts
992
+ function matchesRule(pattern, value, logger) {
993
+ if (!pattern) return true;
994
+ try {
995
+ const re = new RegExp(pattern);
996
+ return re.test(value);
997
+ } catch (err) {
998
+ if (logger) logger.warn(`Invalid firewall regex: ${pattern}`, { error: err?.message });
999
+ return false;
1000
+ }
1001
+ }
1002
+ function evaluateToolFirewall(config, toolName, argsJson, logger) {
1003
+ const defaultDecision = config?.defaultDecision || "ask";
1004
+ const rules = config?.rules || [];
1005
+ for (const rule of rules) {
1006
+ const nameOk = matchesRule(rule.name, toolName, logger);
1007
+ const argsOk = matchesRule(rule.arguments, argsJson, logger);
1008
+ if (nameOk && argsOk) {
1009
+ return {
1010
+ decision: rule.decision,
1011
+ reason: rule.reason || "Matched firewall rule"
1012
+ };
1013
+ }
1014
+ }
1015
+ return {
1016
+ decision: defaultDecision,
1017
+ reason: "Default firewall decision"
1018
+ };
1019
+ }
1020
+
1021
+ // src/media/MediaBridge.ts
1022
+ var MediaBridge = class {
1023
+ adapter;
1024
+ constructor(adapter) {
1025
+ this.adapter = adapter;
1026
+ }
1027
+ transcribe(request) {
1028
+ return this.adapter.transcribe(request);
1029
+ }
1030
+ synthesize(request) {
1031
+ return this.adapter.synthesize(request);
1032
+ }
1033
+ async synthesizeToArray(request) {
1034
+ const chunks = [];
1035
+ for await (const chunk of this.adapter.synthesize(request)) {
1036
+ chunks.push(chunk);
735
1037
  }
1038
+ return chunks;
1039
+ }
1040
+ describeImage(request) {
1041
+ return this.adapter.describeImage(request);
1042
+ }
1043
+ generateImage(request) {
1044
+ return this.adapter.generateImage(request);
1045
+ }
1046
+ getModelInfo() {
1047
+ return this.adapter.getModelInfo();
1048
+ }
1049
+ supportsVision() {
1050
+ return this.adapter.getModelInfo().supportsVision;
736
1051
  }
737
1052
  };
738
1053
 
@@ -1031,146 +1346,1120 @@ var summarizeSandwichTool = {
1031
1346
  }
1032
1347
  };
1033
1348
 
1034
- // src/agent/SessionManager.ts
1035
- var SessionManager = class {
1036
- contextManager;
1037
- toolRegistry;
1038
- skillInjector;
1039
- context;
1040
- adapter;
1041
- config;
1042
- iterations = 0;
1043
- logger;
1044
- constructor(config) {
1045
- this.config = config;
1046
- this.adapter = config.adapter;
1047
- this.logger = config.logger || new DefaultLogger();
1048
- this.contextManager = new ContextManager();
1049
- this.toolRegistry = new ToolRegistry(config.tools || []);
1050
- this.skillInjector = new SkillInjector(config.skills || []);
1051
- for (const strategy of config.compressionStrategies || []) {
1052
- this.contextManager.registerStrategy(strategy);
1349
+ // src/tools/builtin/media.ts
1350
+ var defaultPrefix = "media_";
1351
+ function requireAdapter(context) {
1352
+ if (!context.adapter) {
1353
+ throw new LemuraAdapterError("Provider adapter is missing from tool context", "ADAPTER_MISSING");
1354
+ }
1355
+ return context.adapter;
1356
+ }
1357
+ function createMediaTools(prefix = defaultPrefix) {
1358
+ const p = prefix || defaultPrefix;
1359
+ const transcribeTool = {
1360
+ name: `${p}transcribe`,
1361
+ description: "Transcribe audio from base64 into text.",
1362
+ parameters: {
1363
+ type: "object",
1364
+ properties: {
1365
+ audioBase64: { type: "string", description: "Base64-encoded audio payload." },
1366
+ mimeType: { type: "string", description: "Audio MIME type like audio/wav or audio/mpeg." },
1367
+ language: { type: "string", description: "Optional language hint like en, fr." },
1368
+ model: { type: "string", description: "Optional model override for ASR." }
1369
+ },
1370
+ required: ["audioBase64", "mimeType"]
1371
+ },
1372
+ async execute(params, context) {
1373
+ const adapter = requireAdapter(context);
1374
+ const result = await adapter.transcribe({
1375
+ audioBase64: params.audioBase64,
1376
+ mimeType: params.mimeType,
1377
+ language: params.language,
1378
+ model: params.model
1379
+ });
1380
+ return result;
1053
1381
  }
1054
- if (config.stmRegistry) {
1055
- this.toolRegistry.register(readChunkTool);
1056
- this.toolRegistry.register(searchChunkTool);
1057
- this.toolRegistry.register(listChunksTool);
1058
- this.toolRegistry.register(updateChunkTool);
1059
- this.toolRegistry.register(readScratchpadTool);
1060
- this.toolRegistry.register(writeScratchpadTool);
1061
- this.toolRegistry.register(removeScratchpadTool);
1062
- this.toolRegistry.register(summarizeSandwichTool);
1382
+ };
1383
+ const synthesizeTool = {
1384
+ name: `${p}synthesize`,
1385
+ description: "Synthesize text to speech and return audio chunks (base64).",
1386
+ parameters: {
1387
+ type: "object",
1388
+ properties: {
1389
+ text: { type: "string", description: "Text to synthesize." },
1390
+ voiceId: { type: "string", description: "Voice identifier." },
1391
+ format: { type: "string", enum: ["mp3", "wav", "pcm"] },
1392
+ model: { type: "string", description: "Optional model override for TTS." }
1393
+ },
1394
+ required: ["text", "voiceId", "format"]
1395
+ },
1396
+ async execute(params, context) {
1397
+ const adapter = requireAdapter(context);
1398
+ const chunks = [];
1399
+ for await (const chunk of adapter.synthesize({
1400
+ text: params.text,
1401
+ voiceId: params.voiceId,
1402
+ format: params.format,
1403
+ model: params.model
1404
+ })) {
1405
+ chunks.push(chunk);
1406
+ }
1407
+ return { chunks };
1063
1408
  }
1064
- this.context = {
1065
- systemPrompt: config.systemPrompt || "",
1066
- scratchpad: "",
1067
- turns: [],
1068
- tokenCount: 0,
1069
- maxTokens: config.maxTokens,
1070
- metadata: {}
1409
+ };
1410
+ const visionTool = {
1411
+ name: `${p}describe_image`,
1412
+ description: "Describe an image from base64 and optional prompt.",
1413
+ parameters: {
1414
+ type: "object",
1415
+ properties: {
1416
+ imageBase64: { type: "string", description: "Base64-encoded image payload." },
1417
+ prompt: { type: "string", description: "Optional instruction or focus for description." },
1418
+ model: { type: "string", description: "Optional model override for vision." }
1419
+ },
1420
+ required: ["imageBase64"]
1421
+ },
1422
+ async execute(params, context) {
1423
+ const adapter = requireAdapter(context);
1424
+ const result = await adapter.describeImage({
1425
+ imageBase64: params.imageBase64,
1426
+ prompt: params.prompt,
1427
+ model: params.model
1428
+ });
1429
+ return result;
1430
+ }
1431
+ };
1432
+ const imageGenTool = {
1433
+ name: `${p}generate_image`,
1434
+ description: "Generate an image from a prompt.",
1435
+ parameters: {
1436
+ type: "object",
1437
+ properties: {
1438
+ prompt: { type: "string", description: "Text prompt for the image." },
1439
+ dimensions: { type: "string", description: "Image size like 1024x1024." },
1440
+ model: { type: "string", description: "Optional model override for image generation." }
1441
+ },
1442
+ required: ["prompt", "dimensions"]
1443
+ },
1444
+ async execute(params, context) {
1445
+ const adapter = requireAdapter(context);
1446
+ const result = await adapter.generateImage({
1447
+ prompt: params.prompt,
1448
+ dimensions: params.dimensions,
1449
+ model: params.model
1450
+ });
1451
+ return result;
1452
+ }
1453
+ };
1454
+ return [transcribeTool, synthesizeTool, visionTool, imageGenTool];
1455
+ }
1456
+
1457
+ // src/agent/execution/StepCounter.ts
1458
+ var StepCounter = class {
1459
+ constructor(maxSteps = 20) {
1460
+ this.maxSteps = maxSteps;
1461
+ }
1462
+ toolCallCount = 0;
1463
+ increment(count = 1) {
1464
+ this.toolCallCount += count;
1465
+ }
1466
+ get count() {
1467
+ return this.toolCallCount;
1468
+ }
1469
+ isMaxReached() {
1470
+ return this.toolCallCount >= this.maxSteps;
1471
+ }
1472
+ getForcedConclusionPrompt() {
1473
+ return `You have used ${this.toolCallCount}/${this.maxSteps} steps. Provide your final response now.
1474
+ Do not call any more tools. Use the required structure below.`;
1475
+ }
1476
+ };
1477
+
1478
+ // src/agent/execution/FinalResponseFormatter.ts
1479
+ var FinalResponseFormatter = class {
1480
+ static getRequiredStructure() {
1481
+ return `## Goal Status: [ACHIEVED | PARTIALLY_ACHIEVED | FAILED]
1482
+
1483
+ ### What was accomplished
1484
+ [Summary of completed work]
1485
+
1486
+ ### Remaining tasks
1487
+ [Bulleted list, or "None"]
1488
+
1489
+ ### Failed steps
1490
+ [Tool/step name + error context, or "None"]
1491
+
1492
+ ### Result
1493
+ [The actual answer or deliverable]`;
1494
+ }
1495
+ static validateStructure(response) {
1496
+ if (!response.includes("## Goal Status:")) return false;
1497
+ if (!response.includes("### What was accomplished")) return false;
1498
+ if (!response.includes("### Remaining tasks")) return false;
1499
+ if (!response.includes("### Failed steps")) return false;
1500
+ if (!response.includes("### Result")) return false;
1501
+ return true;
1502
+ }
1503
+ };
1504
+
1505
+ // src/agent/execution/ToolResponseProcessor.ts
1506
+ var ToolResponseProcessor = class {
1507
+ evaluate(response, tool, context) {
1508
+ const length = response.length;
1509
+ const estimatedTokens = length / 4;
1510
+ let sizeClass = "small";
1511
+ if (estimatedTokens > 2e3) sizeClass = "oversized";
1512
+ else if (estimatedTokens > 800) sizeClass = "large";
1513
+ else if (estimatedTokens > 200) sizeClass = "medium";
1514
+ return {
1515
+ relevanceScore: 1,
1516
+ sizeClass,
1517
+ shouldCompress: sizeClass === "large" || sizeClass === "oversized",
1518
+ suggestedMaxTokens: 500,
1519
+ answered: true,
1520
+ answeredPartially: false,
1521
+ errorDetected: response.toLowerCase().includes("error"),
1522
+ suggestedAction: "continue"
1071
1523
  };
1072
1524
  }
1073
- getContext() {
1074
- return { ...this.context };
1525
+ compress(response, evaluation) {
1526
+ if (!evaluation.shouldCompress || evaluation.errorDetected) {
1527
+ return response;
1528
+ }
1529
+ if (response.length > 1e3) {
1530
+ return response.substring(0, 1e3) + "\n...[COMPRESSED TO SAVE TOKENS]...";
1531
+ }
1532
+ return response;
1075
1533
  }
1076
- getHistory() {
1077
- return [...this.context.turns];
1534
+ };
1535
+
1536
+ // src/agent/execution/GoalInjector.ts
1537
+ var GoalInjector = class {
1538
+ constructor(goal) {
1539
+ this.goal = goal;
1078
1540
  }
1079
- async run(userMessage) {
1080
- this.logger.info(`Starting new session run`, {
1081
- model: this.config.model,
1082
- message: Array.isArray(userMessage) ? "[Multimodal Content]" : userMessage
1083
- });
1084
- this.context.turns.push({
1085
- role: "user",
1086
- content: userMessage,
1087
- tokenCount: Array.isArray(userMessage) ? userMessage.length * 50 : this.adapter.estimateTokens(userMessage),
1088
- turnIndex: this.context.turns.length,
1089
- compressed: false
1090
- });
1091
- const maxIts = this.config.maxIterations || 10;
1092
- this.iterations = 0;
1093
- while (this.iterations < maxIts) {
1094
- this.iterations++;
1095
- this.logger.debug(`ReAct Iteration ${this.iterations}/${maxIts}`);
1096
- this.context = await this.contextManager.prepare(this.context);
1097
- const messages = this.context.turns.map((t) => ({
1098
- role: t.role,
1099
- content: t.content,
1100
- ...t.role === "tool" && t.toolResults?.[0] ? { name: t.toolResults[0].toolCallId } : {},
1101
- ...t.role === "assistant" && t.toolCalls ? { toolCalls: t.toolCalls } : {}
1102
- }));
1103
- let fullSystemPrompt = this.context.systemPrompt || "";
1104
- const injectedSkills = this.skillInjector.buildInjectionBlock("system_prompt");
1105
- if (injectedSkills) {
1106
- fullSystemPrompt += "\n\n" + injectedSkills;
1107
- }
1108
- if (fullSystemPrompt.trim()) {
1109
- messages.unshift({ role: "system", content: fullSystemPrompt.trim() });
1541
+ injectInto(prompt) {
1542
+ const formatting = `[CURRENT GOAL]
1543
+ ${this.goal.statement}
1544
+
1545
+ Success criteria:
1546
+ ${this.goal.successCriteria.map((c) => `- ${c}`).join("\n")}
1547
+ [/CURRENT GOAL]`;
1548
+ if (this.goal.injectionPosition === "system_prompt") {
1549
+ return `${prompt}
1550
+
1551
+ ${formatting}`;
1552
+ }
1553
+ return prompt;
1554
+ }
1555
+ getGoal() {
1556
+ return { ...this.goal };
1557
+ }
1558
+ };
1559
+ var MCPClient = class {
1560
+ config;
1561
+ logger;
1562
+ _serverName;
1563
+ timeoutMs;
1564
+ _tools = [];
1565
+ _connected = false;
1566
+ // stdio-specific state
1567
+ process = null;
1568
+ pendingCallbacks = /* @__PURE__ */ new Map();
1569
+ requestId = 1;
1570
+ stdioBuffer = "";
1571
+ constructor(name, config, logger) {
1572
+ this._serverName = name;
1573
+ this.config = config;
1574
+ this.logger = logger;
1575
+ this.timeoutMs = config.timeoutMs ?? 3e4;
1576
+ }
1577
+ get serverName() {
1578
+ return this._serverName;
1579
+ }
1580
+ get tools() {
1581
+ return this._tools;
1582
+ }
1583
+ get isConnected() {
1584
+ return this._connected;
1585
+ }
1586
+ // ---------------------------------------------------------------------------
1587
+ // Public API
1588
+ // ---------------------------------------------------------------------------
1589
+ /**
1590
+ * Connects to the MCP server: sends `initialize` + `notifications/initialized`,
1591
+ * then fetches the tool list via `tools/list`.
1592
+ */
1593
+ async connect() {
1594
+ this.logger.debug(`[MCP:${this._serverName}] Connecting via ${this.config.transport}...`);
1595
+ if (this.config.transport === "stdio") {
1596
+ await this._connectStdio();
1597
+ } else {
1598
+ if (!this.config.url) {
1599
+ throw new LemuraMCPConnectionError(
1600
+ `[MCP:${this._serverName}] 'url' is required for ${this.config.transport} transport`,
1601
+ `No URL provided for MCP server '${this._serverName}'`,
1602
+ [`Set 'url' in the MCPServerConfig for server '${this._serverName}'`]
1603
+ );
1604
+ }
1605
+ }
1606
+ await this._initialize();
1607
+ await this._discoverTools();
1608
+ this._connected = true;
1609
+ this.logger.info(`[MCP:${this._serverName}] Connected. ${this._tools.length} tool(s) available.`);
1610
+ }
1611
+ /**
1612
+ * Calls a tool on this MCP server.
1613
+ *
1614
+ * @param toolName - The tool name as declared by the server
1615
+ * @param args - Tool arguments (must match the tool's inputSchema)
1616
+ * @returns The serialised result from the server
1617
+ */
1618
+ async callTool(toolName, args) {
1619
+ this.logger.debug(`[MCP:${this._serverName}] Calling tool '${toolName}'`, { args });
1620
+ const request = {
1621
+ jsonrpc: "2.0",
1622
+ id: this._nextId(),
1623
+ method: "tools/call",
1624
+ params: { name: toolName, arguments: args }
1625
+ };
1626
+ const response = await this._rpc(request);
1627
+ this._assertNoError(response, `tool '${toolName}'`);
1628
+ const result = response.result;
1629
+ if (result && Array.isArray(result["content"])) {
1630
+ const texts = result["content"].filter((b) => b.type === "text" && b.text).map((b) => b.text);
1631
+ return texts.length === 1 ? texts[0] : texts.length > 1 ? texts.join("\n") : result;
1632
+ }
1633
+ return result;
1634
+ }
1635
+ /**
1636
+ * Disconnects from the MCP server.
1637
+ * For stdio: sends the `exit` notification then terminates the process.
1638
+ */
1639
+ async disconnect() {
1640
+ if (!this._connected) return;
1641
+ this.logger.debug(`[MCP:${this._serverName}] Disconnecting...`);
1642
+ this._connected = false;
1643
+ if (this.process) {
1644
+ try {
1645
+ const exitNotif = JSON.stringify({
1646
+ jsonrpc: "2.0",
1647
+ method: "notifications/exit"
1648
+ }) + "\n";
1649
+ this.process.stdin?.write(exitNotif);
1650
+ } catch {
1651
+ }
1652
+ this.process.kill();
1653
+ this.process = null;
1654
+ }
1655
+ for (const [, cb] of this.pendingCallbacks) {
1656
+ cb.reject(new LemuraMCPConnectionError(
1657
+ `[MCP:${this._serverName}] Connection closed before response received`
1658
+ ));
1659
+ }
1660
+ this.pendingCallbacks.clear();
1661
+ }
1662
+ // ---------------------------------------------------------------------------
1663
+ // Private: stdio transport
1664
+ // ---------------------------------------------------------------------------
1665
+ async _connectStdio() {
1666
+ const { command, args = [], env = {} } = this.config;
1667
+ if (!command) {
1668
+ throw new LemuraMCPConnectionError(
1669
+ `[MCP:${this._serverName}] 'command' is required for stdio transport`,
1670
+ `No command provided for stdio MCP server '${this._serverName}'`,
1671
+ [`Set the 'command' field in MCPServerConfig for server '${this._serverName}'`]
1672
+ );
1673
+ }
1674
+ return new Promise((resolve, reject) => {
1675
+ let proc;
1676
+ try {
1677
+ proc = child_process.spawn(command, args, {
1678
+ stdio: ["pipe", "pipe", "pipe"],
1679
+ env: { ...process.env, ...env }
1680
+ });
1681
+ } catch (err) {
1682
+ return reject(new LemuraMCPConnectionError(
1683
+ `[MCP:${this._serverName}] Failed to spawn '${command}': ${err.message}`,
1684
+ `Could not start MCP server process for '${this._serverName}'`,
1685
+ [
1686
+ `Ensure '${command}' is installed and available in PATH`,
1687
+ `Check the 'args' and 'env' configuration for server '${this._serverName}'`
1688
+ ]
1689
+ ));
1690
+ }
1691
+ this.process = proc;
1692
+ proc.on("error", (err) => {
1693
+ this.logger.error(`[MCP:${this._serverName}] Process error: ${err.message}`);
1694
+ for (const [, cb] of this.pendingCallbacks) {
1695
+ cb.reject(new LemuraMCPConnectionError(
1696
+ `[MCP:${this._serverName}] Process error: ${err.message}`
1697
+ ));
1698
+ }
1699
+ this.pendingCallbacks.clear();
1700
+ this._connected = false;
1701
+ });
1702
+ proc.on("exit", (code, signal) => {
1703
+ if (this._connected) {
1704
+ this.logger.warn(`[MCP:${this._serverName}] Process exited unexpectedly (code=${code}, signal=${signal})`);
1705
+ this._connected = false;
1706
+ }
1707
+ });
1708
+ proc.stdout?.on("data", (chunk) => {
1709
+ this.stdioBuffer += chunk.toString("utf8");
1710
+ this._flushStdioBuffer();
1711
+ });
1712
+ proc.stderr?.on("data", (chunk) => {
1713
+ this.logger.debug(`[MCP:${this._serverName}] stderr: ${chunk.toString("utf8").trim()}`);
1714
+ });
1715
+ resolve();
1716
+ });
1717
+ }
1718
+ /** Parses newline-delimited JSON messages from the stdio buffer */
1719
+ _flushStdioBuffer() {
1720
+ const lines = this.stdioBuffer.split("\n");
1721
+ this.stdioBuffer = lines.pop() ?? "";
1722
+ for (const line of lines) {
1723
+ const trimmed = line.trim();
1724
+ if (!trimmed) continue;
1725
+ let msg;
1726
+ try {
1727
+ msg = JSON.parse(trimmed);
1728
+ } catch {
1729
+ this.logger.debug(`[MCP:${this._serverName}] Ignoring non-JSON line: ${trimmed.slice(0, 120)}`);
1730
+ continue;
1731
+ }
1732
+ if (msg.id === void 0 || msg.id === null) continue;
1733
+ const cb = this.pendingCallbacks.get(msg.id);
1734
+ if (cb) {
1735
+ this.pendingCallbacks.delete(msg.id);
1736
+ cb.resolve(msg);
1737
+ }
1738
+ }
1739
+ }
1740
+ // ---------------------------------------------------------------------------
1741
+ // Private: JSON-RPC helpers
1742
+ // ---------------------------------------------------------------------------
1743
+ _nextId() {
1744
+ return this.requestId++;
1745
+ }
1746
+ /** Sends a JSON-RPC request and returns the response, honouring the configured timeout. */
1747
+ async _rpc(request) {
1748
+ if (this.config.transport === "stdio") {
1749
+ return this._rpcStdio(request);
1750
+ }
1751
+ return this._rpcHttp(request);
1752
+ }
1753
+ _rpcStdio(request) {
1754
+ return new Promise((resolve, reject) => {
1755
+ if (!this.process?.stdin) {
1756
+ return reject(new LemuraMCPConnectionError(
1757
+ `[MCP:${this._serverName}] stdin is not available`
1758
+ ));
1759
+ }
1760
+ const id = request.id;
1761
+ let timedOut = false;
1762
+ const timer = setTimeout(() => {
1763
+ timedOut = true;
1764
+ this.pendingCallbacks.delete(id);
1765
+ reject(new LemuraMCPTimeoutError(
1766
+ `[MCP:${this._serverName}] RPC call '${request.method}' timed out after ${this.timeoutMs}ms`
1767
+ ));
1768
+ }, this.timeoutMs);
1769
+ this.pendingCallbacks.set(id, {
1770
+ resolve: (value) => {
1771
+ if (timedOut) return;
1772
+ clearTimeout(timer);
1773
+ resolve(value);
1774
+ },
1775
+ reject: (err) => {
1776
+ if (timedOut) return;
1777
+ clearTimeout(timer);
1778
+ reject(err);
1779
+ }
1780
+ });
1781
+ this.process.stdin.write(JSON.stringify(request) + "\n", "utf8");
1782
+ });
1783
+ }
1784
+ async _rpcHttp(request) {
1785
+ const url = this.config.url;
1786
+ const controller = new AbortController();
1787
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
1788
+ try {
1789
+ const res = await fetch(url, {
1790
+ method: "POST",
1791
+ headers: { "Content-Type": "application/json", "Accept": "application/json" },
1792
+ body: JSON.stringify(request),
1793
+ signal: controller.signal
1794
+ });
1795
+ if (!res.ok) {
1796
+ throw new LemuraMCPConnectionError(
1797
+ `[MCP:${this._serverName}] HTTP ${res.status} from server: ${res.statusText}`,
1798
+ `MCP server '${this._serverName}' returned a non-2xx response`,
1799
+ [`Verify the server is running at '${url}' and accepting JSON-RPC requests`]
1800
+ );
1801
+ }
1802
+ return await res.json();
1803
+ } catch (err) {
1804
+ if (err.name === "AbortError") {
1805
+ throw new LemuraMCPTimeoutError(
1806
+ `[MCP:${this._serverName}] HTTP RPC '${request.method}' timed out after ${this.timeoutMs}ms`
1807
+ );
1808
+ }
1809
+ throw err;
1810
+ } finally {
1811
+ clearTimeout(timer);
1812
+ }
1813
+ }
1814
+ /** Sends `initialize` then `notifications/initialized` per MCP spec */
1815
+ async _initialize() {
1816
+ const initRequest = {
1817
+ jsonrpc: "2.0",
1818
+ id: this._nextId(),
1819
+ method: "initialize",
1820
+ params: {
1821
+ protocolVersion: "2024-11-05",
1822
+ capabilities: { tools: {} },
1823
+ clientInfo: { name: "lemura", version: "1.2.0" }
1824
+ }
1825
+ };
1826
+ let response;
1827
+ try {
1828
+ response = await this._rpc(initRequest);
1829
+ } catch (err) {
1830
+ const msg = err.message ?? String(err);
1831
+ throw new LemuraMCPConnectionError(
1832
+ `[MCP:${this._serverName}] Initialize failed: ${msg}`,
1833
+ `Failed to initialize MCP server '${this._serverName}'`,
1834
+ [
1835
+ `Ensure the server supports the MCP protocol`,
1836
+ `Verify the server is running and accepting connections`
1837
+ ]
1838
+ );
1839
+ }
1840
+ this._assertNoError(response, "initialize");
1841
+ const initializedNotif = JSON.stringify({
1842
+ jsonrpc: "2.0",
1843
+ method: "notifications/initialized"
1844
+ }) + "\n";
1845
+ if (this.config.transport === "stdio" && this.process?.stdin) {
1846
+ this.process.stdin.write(initializedNotif, "utf8");
1847
+ } else if (this.config.transport !== "stdio") {
1848
+ fetch(this.config.url, {
1849
+ method: "POST",
1850
+ headers: { "Content-Type": "application/json" },
1851
+ body: initializedNotif
1852
+ }).catch(() => {
1853
+ });
1854
+ }
1855
+ }
1856
+ /** Fetches and caches the tool list from the server */
1857
+ async _discoverTools() {
1858
+ const request = {
1859
+ jsonrpc: "2.0",
1860
+ id: this._nextId(),
1861
+ method: "tools/list",
1862
+ params: {}
1863
+ };
1864
+ const response = await this._rpc(request);
1865
+ this._assertNoError(response, "tools/list");
1866
+ const result = response.result;
1867
+ const tools = result?.["tools"] ?? [];
1868
+ this._tools = tools;
1869
+ }
1870
+ _assertNoError(response, context) {
1871
+ if (response.error) {
1872
+ throw new LemuraMCPConnectionError(
1873
+ `[MCP:${this._serverName}] JSON-RPC error in ${context}: [${response.error.code}] ${response.error.message}`,
1874
+ `MCP server '${this._serverName}' returned a JSON-RPC error during ${context}`,
1875
+ ["Check the MCP server logs for more details"]
1876
+ );
1877
+ }
1878
+ }
1879
+ };
1880
+
1881
+ // src/mcp/MCPClientRegistry.ts
1882
+ var MCPClientRegistry = class {
1883
+ clients = /* @__PURE__ */ new Map();
1884
+ /** toolName → serverName */
1885
+ toolRouter = /* @__PURE__ */ new Map();
1886
+ logger;
1887
+ constructor(logger) {
1888
+ this.logger = logger;
1889
+ }
1890
+ // ---------------------------------------------------------------------------
1891
+ // Registration
1892
+ // ---------------------------------------------------------------------------
1893
+ /**
1894
+ * Creates an `MCPClient` for the given config, connects to the server, and
1895
+ * registers the server by name.
1896
+ *
1897
+ * @throws {LemuraMCPConnectionError} if connection / initialization fails
1898
+ */
1899
+ async register(name, config) {
1900
+ if (this.clients.has(name)) {
1901
+ this.logger.warn(`[MCPClientRegistry] Server '${name}' is already registered \u2014 skipping`);
1902
+ return;
1903
+ }
1904
+ const client = new MCPClient(name, config, this.logger);
1905
+ try {
1906
+ await client.connect();
1907
+ } catch (err) {
1908
+ const msg = err.message ?? String(err);
1909
+ throw new LemuraMCPConnectionError(
1910
+ `[MCPClientRegistry] Failed to connect to server '${name}': ${msg}`,
1911
+ `Could not establish connection to MCP server '${name}'`,
1912
+ [
1913
+ `Verify the server is reachable and the transport config is correct`,
1914
+ `Check the 'command', 'args', or 'url' for server '${name}'`
1915
+ ]
1916
+ );
1917
+ }
1918
+ this.clients.set(name, client);
1919
+ this.logger.info(`[MCPClientRegistry] Server '${name}' registered (${client.tools.length} tools)`);
1920
+ }
1921
+ // ---------------------------------------------------------------------------
1922
+ // Tool discovery
1923
+ // ---------------------------------------------------------------------------
1924
+ /**
1925
+ * Collects tools from all connected MCP servers and returns them as
1926
+ * `IToolDefinition` adapters ready to be registered in `ToolRegistry`.
1927
+ *
1928
+ * Tool names are **not** namespaced — if two servers expose a tool with the
1929
+ * same name, the last-registered server wins and a warning is emitted.
1930
+ */
1931
+ async discoverTools() {
1932
+ const bridged = [];
1933
+ for (const [serverName, client] of this.clients) {
1934
+ for (const mcpTool of client.tools) {
1935
+ if (this.toolRouter.has(mcpTool.name)) {
1936
+ this.logger.warn(
1937
+ `[MCPClientRegistry] Tool '${mcpTool.name}' is already provided by '${this.toolRouter.get(mcpTool.name)}'; overriding with server '${serverName}'`
1938
+ );
1939
+ }
1940
+ this.toolRouter.set(mcpTool.name, serverName);
1941
+ bridged.push(this._bridge(mcpTool, serverName));
1942
+ }
1943
+ }
1944
+ this.logger.debug(`[MCPClientRegistry] Discovered ${bridged.length} total MCP tool(s)`);
1945
+ return bridged;
1946
+ }
1947
+ // ---------------------------------------------------------------------------
1948
+ // Routing
1949
+ // ---------------------------------------------------------------------------
1950
+ /**
1951
+ * Calls a tool on the correct MCP server.
1952
+ *
1953
+ * @param toolName - The tool name as registered via `discoverTools()`
1954
+ * @param args - Parsed arguments object
1955
+ * @throws {LemuraMCPConnectionError} if no server is registered for `toolName`
1956
+ */
1957
+ async callTool(toolName, args) {
1958
+ const serverName = this.toolRouter.get(toolName);
1959
+ if (!serverName) {
1960
+ throw new LemuraMCPConnectionError(
1961
+ `[MCPClientRegistry] No MCP server found for tool '${toolName}'`,
1962
+ `Tool '${toolName}' was not discovered from any connected MCP server`,
1963
+ ["Verify the MCP server that provides this tool is configured and connected"]
1964
+ );
1965
+ }
1966
+ const client = this.clients.get(serverName);
1967
+ return client.callTool(toolName, args);
1968
+ }
1969
+ // ---------------------------------------------------------------------------
1970
+ // Lifecycle
1971
+ // ---------------------------------------------------------------------------
1972
+ /**
1973
+ * Disconnects all registered MCP servers gracefully.
1974
+ * Called automatically by `SessionManager.close()`.
1975
+ */
1976
+ async disconnectAll() {
1977
+ const names = Array.from(this.clients.keys());
1978
+ this.logger.debug(`[MCPClientRegistry] Disconnecting ${names.length} MCP server(s)...`);
1979
+ await Promise.allSettled(
1980
+ names.map(async (name) => {
1981
+ try {
1982
+ await this.clients.get(name).disconnect();
1983
+ this.logger.debug(`[MCPClientRegistry] '${name}' disconnected`);
1984
+ } catch (err) {
1985
+ this.logger.warn(
1986
+ `[MCPClientRegistry] Error disconnecting '${name}': ${err.message}`
1987
+ );
1988
+ }
1989
+ })
1990
+ );
1991
+ this.clients.clear();
1992
+ this.toolRouter.clear();
1993
+ }
1994
+ /** Returns the names of all currently registered servers */
1995
+ getRegisteredServers() {
1996
+ return Array.from(this.clients.keys());
1997
+ }
1998
+ // ---------------------------------------------------------------------------
1999
+ // Private helpers
2000
+ // ---------------------------------------------------------------------------
2001
+ /**
2002
+ * Bridges an `MCPToolDefinition` from the protocol into an `IToolDefinition`
2003
+ * that the existing `ToolRegistry` can execute.
2004
+ */
2005
+ _bridge(mcpTool, serverName) {
2006
+ const self = this;
2007
+ return {
2008
+ name: mcpTool.name,
2009
+ description: `[MCP:${serverName}] ${mcpTool.description}`,
2010
+ parameters: mcpTool.inputSchema ?? { type: "object", properties: {} },
2011
+ async execute(params, _context) {
2012
+ return self.callTool(mcpTool.name, params ?? {});
2013
+ }
2014
+ };
2015
+ }
2016
+ };
2017
+
2018
+ // src/agent/SessionManager.ts
2019
+ var SessionManager = class {
2020
+ contextManager;
2021
+ toolRegistry;
2022
+ skillInjector;
2023
+ context;
2024
+ adapter;
2025
+ config;
2026
+ iterations = 0;
2027
+ logger;
2028
+ media;
2029
+ // Advanced execution state
2030
+ stepCounter;
2031
+ toolResponseProcessor;
2032
+ goalInjector = null;
2033
+ continuationPlanner = null;
2034
+ // MCP
2035
+ mcpRegistry = null;
2036
+ /** Resolves when all MCP servers are connected; used by run() and stream() */
2037
+ mcpReady = null;
2038
+ // Tool execution budget tracking
2039
+ totalToolCallCount = 0;
2040
+ perToolCallCount = /* @__PURE__ */ new Map();
2041
+ totalTokens = 0;
2042
+ constructor(config) {
2043
+ this.config = config;
2044
+ this.adapter = config.adapter;
2045
+ this.logger = config.logger || new DefaultLogger();
2046
+ this.contextManager = new ContextManager();
2047
+ this.toolRegistry = new ToolRegistry(config.tools || [], {
2048
+ defaultTimeoutMs: config.toolRegistryTimeoutMs ?? 3e4
2049
+ });
2050
+ this.skillInjector = new SkillInjector(config.skills || []);
2051
+ this.media = new MediaBridge(this.adapter);
2052
+ this.stepCounter = new StepCounter(config.maxSteps ?? 20);
2053
+ this.toolResponseProcessor = config.toolResponseProcessor ?? new ToolResponseProcessor();
2054
+ for (const strategy of config.compressionStrategies || []) {
2055
+ this.contextManager.registerStrategy(strategy);
2056
+ }
2057
+ if (config.stmRegistry) {
2058
+ this.toolRegistry.register(readChunkTool);
2059
+ this.toolRegistry.register(searchChunkTool);
2060
+ this.toolRegistry.register(listChunksTool);
2061
+ this.toolRegistry.register(updateChunkTool);
2062
+ this.toolRegistry.register(readScratchpadTool);
2063
+ this.toolRegistry.register(writeScratchpadTool);
2064
+ this.toolRegistry.register(removeScratchpadTool);
2065
+ this.toolRegistry.register(summarizeSandwichTool);
2066
+ }
2067
+ if (config.media?.enableTools) {
2068
+ const prefix = config.media.toolPrefix || "media_";
2069
+ for (const tool of createMediaTools(prefix)) {
2070
+ this.toolRegistry.register(tool);
2071
+ }
2072
+ }
2073
+ this.context = {
2074
+ systemPrompt: config.systemPrompt || "",
2075
+ scratchpad: "",
2076
+ turns: [],
2077
+ tokenCount: 0,
2078
+ maxTokens: config.maxTokens,
2079
+ metadata: {}
2080
+ };
2081
+ this.goalInjector = null;
2082
+ if (config.mcpServers && config.mcpServers.length > 0) {
2083
+ this.mcpRegistry = new MCPClientRegistry(this.logger);
2084
+ this.mcpReady = this._initMCP(config.mcpServers);
2085
+ }
2086
+ this.emitTrace("system", "session_init", {
2087
+ config: {
2088
+ model: this.config.model,
2089
+ maxIterations: this.config.maxIterations,
2090
+ maxSteps: this.config.maxSteps,
2091
+ parallelToolCalls: this.config.parallelToolCalls,
2092
+ enableGoalPlanning: this.config.enableGoalPlanning,
2093
+ enableContinuationPlanning: this.config.enableContinuationPlanning
2094
+ }
2095
+ });
2096
+ }
2097
+ emitTrace(type, name, metadata, input, output, status = "done") {
2098
+ if (this.config.onTrace) {
2099
+ this.config.onTrace({
2100
+ type,
2101
+ name,
2102
+ metadata: metadata || {},
2103
+ input,
2104
+ output,
2105
+ status,
2106
+ startedAt: Date.now()
2107
+ });
2108
+ }
2109
+ }
2110
+ /**
2111
+ * Returns a shallow copy of the current context window.
2112
+ */
2113
+ getContext() {
2114
+ return { ...this.context };
2115
+ }
2116
+ /**
2117
+ * Returns the current conversation history.
2118
+ */
2119
+ getHistory() {
2120
+ return [...this.context.turns];
2121
+ }
2122
+ /**
2123
+ * Returns the `MediaBridge` for direct ASR / TTS / Vision / Image-gen calls.
2124
+ */
2125
+ getMedia() {
2126
+ return this.media;
2127
+ }
2128
+ // -----------------------------------------------------------------------
2129
+ // MCP initialisation
2130
+ // -----------------------------------------------------------------------
2131
+ /**
2132
+ * Connects all configured MCP servers and registers their bridged tools.
2133
+ * Called from the constructor as a fire-and-start async task; `run()` and
2134
+ * `stream()` await `this.mcpReady` before executing.
2135
+ */
2136
+ async _initMCP(mcpServers) {
2137
+ if (!this.mcpRegistry) return;
2138
+ this.emitTrace("system", "mcp_init_start", { serverCount: mcpServers.length });
2139
+ this.logger.info(`[MCP] Connecting to ${mcpServers.length} server(s)...`);
2140
+ for (const serverConfig of mcpServers) {
2141
+ try {
2142
+ await this.mcpRegistry.register(serverConfig.name, serverConfig);
2143
+ this.emitTrace("system", "mcp_server_connected", { server: serverConfig.name });
2144
+ } catch (err) {
2145
+ const msg = err.message ?? String(err);
2146
+ this.logger.error(`[MCP] Failed to connect '${serverConfig.name}': ${msg}`);
2147
+ this.emitTrace("error", "mcp_server_failed", { server: serverConfig.name, error: msg });
2148
+ }
2149
+ }
2150
+ const bridgedTools = await this.mcpRegistry.discoverTools();
2151
+ for (const tool of bridgedTools) {
2152
+ try {
2153
+ this.toolRegistry.register(tool);
2154
+ } catch {
2155
+ this.logger.warn(`[MCP] Tool '${tool.name}' conflicts with an existing tool \u2014 skipping`);
2156
+ }
2157
+ }
2158
+ this.logger.info(`[MCP] ${bridgedTools.length} MCP tool(s) registered`);
2159
+ this.emitTrace("system", "mcp_init_done", {
2160
+ servers: this.mcpRegistry.getRegisteredServers(),
2161
+ toolCount: bridgedTools.length
2162
+ });
2163
+ }
2164
+ // -----------------------------------------------------------------------
2165
+ // Internal helpers
2166
+ // -----------------------------------------------------------------------
2167
+ /** Builds the system prompt, injecting skills and goal if configured. */
2168
+ buildSystemPrompt(userMessage) {
2169
+ let prompt = this.context.systemPrompt || "";
2170
+ if (this.config.enableGoalPlanning && userMessage) {
2171
+ if (!this.goalInjector) {
2172
+ this.goalInjector = new GoalInjector({
2173
+ id: "main",
2174
+ statement: typeof userMessage === "string" ? userMessage : "[multimodal]",
2175
+ decomposition: [],
2176
+ successCriteria: ["The user request is fully answered"],
2177
+ injectionFrequency: this.config.goalInjectionFrequency ?? "always",
2178
+ injectionPosition: this.config.goalInjectionPosition ?? "system_prompt"
2179
+ });
2180
+ this.logger.debug("Goal injector initialised");
2181
+ const goal = this.goalInjector.getGoal();
2182
+ this.emitTrace("planning", "goal_init", {
2183
+ statement: goal.statement,
2184
+ criteria: goal.successCriteria
2185
+ });
2186
+ }
2187
+ if (this.config.goalInjectionPosition !== "pre_turn") {
2188
+ prompt = this.goalInjector.injectInto(prompt);
2189
+ this.emitTrace("planning", "goal_injected", { position: this.config.goalInjectionPosition || "system_prompt" });
2190
+ }
2191
+ }
2192
+ const injectedSkills = this.skillInjector.buildInjectionBlock("system_prompt");
2193
+ if (injectedSkills) {
2194
+ prompt += "\n\n" + injectedSkills;
2195
+ }
2196
+ return prompt.trim();
2197
+ }
2198
+ /** Builds the messages array for the provider from the current context */
2199
+ buildMessages(systemPrompt) {
2200
+ const messages = this.context.turns.map((t) => ({
2201
+ role: t.role,
2202
+ content: t.content,
2203
+ ...t.role === "tool" && t.toolResults?.[0] ? { name: t.toolResults[0].toolCallId } : {},
2204
+ ...t.role === "assistant" && t.toolCalls ? { toolCalls: t.toolCalls } : {}
2205
+ }));
2206
+ if (systemPrompt) {
2207
+ messages.unshift({ role: "system", content: systemPrompt });
2208
+ }
2209
+ if (this.goalInjector && this.config.goalInjectionPosition === "pre_turn") {
2210
+ const goalBlock = this.goalInjector.injectInto("");
2211
+ messages.push({ role: "system", content: goalBlock });
2212
+ this.emitTrace("planning", "goal_injected", { position: "pre_turn" });
2213
+ }
2214
+ return messages;
2215
+ }
2216
+ /** Checks the tool execution budget and throws descriptively if exceeded */
2217
+ checkExecutionBudget(toolName) {
2218
+ const budget = this.config.toolExecutionBudget;
2219
+ if (!budget) return;
2220
+ if (budget.maxCallsPerSession !== void 0 && this.totalToolCallCount >= budget.maxCallsPerSession) {
2221
+ const err = new LemuraMaxIterationsError(
2222
+ `Tool execution budget exceeded: session limit of ${budget.maxCallsPerSession} tool calls reached`
2223
+ );
2224
+ this.logger.warn(err.message);
2225
+ throw err;
2226
+ }
2227
+ if (budget.maxCallsPerTool?.[toolName] !== void 0) {
2228
+ const current = this.perToolCallCount.get(toolName) ?? 0;
2229
+ if (current >= (budget.maxCallsPerTool[toolName] ?? Infinity)) {
2230
+ const err = new LemuraMaxIterationsError(
2231
+ `Tool execution budget exceeded: '${toolName}' has reached its per-tool limit of ${budget.maxCallsPerTool[toolName]}`
2232
+ );
2233
+ this.logger.warn(err.message);
2234
+ this.emitTrace("budget", "tool_limit_exceeded", { toolName, limit: budget.maxCallsPerTool[toolName], totalTokens: this.totalTokens });
2235
+ throw err;
2236
+ }
2237
+ }
2238
+ this.emitTrace("budget", "check_passed", {
2239
+ toolName,
2240
+ totalCalls: this.totalToolCallCount,
2241
+ totalTokens: this.totalTokens,
2242
+ tokenBudgetRemaining: this.config.maxTokens - this.totalTokens
2243
+ });
2244
+ }
2245
+ /** Records a tool call in budget counters */
2246
+ recordToolCall(toolName) {
2247
+ this.totalToolCallCount++;
2248
+ this.perToolCallCount.set(toolName, (this.perToolCallCount.get(toolName) ?? 0) + 1);
2249
+ }
2250
+ /**
2251
+ * Processes a firewall decision for a tool call.
2252
+ * Returns true to proceed, false to block.
2253
+ */
2254
+ async passesFirewall(toolName, argsJson, toolCallId, toolResults) {
2255
+ const firewall = evaluateToolFirewall(
2256
+ this.config.toolFirewall,
2257
+ toolName,
2258
+ argsJson,
2259
+ this.logger
2260
+ );
2261
+ if (firewall.decision === "deny") {
2262
+ this.logger.warn(`Tool blocked by firewall: ${toolName}`, { reason: firewall.reason });
2263
+ toolResults.push({ toolCallId, content: `Blocked by tool firewall: ${firewall.reason}` });
2264
+ return false;
2265
+ }
2266
+ if (firewall.decision === "ask") {
2267
+ if (this.config.toolFirewall?.onAsk) {
2268
+ const userDecision = await this.config.toolFirewall.onAsk(toolName, argsJson);
2269
+ if (userDecision === "deny") {
2270
+ this.logger.warn(`Tool blocked by firewall (ask \u2192 deny): ${toolName}`, { reason: firewall.reason });
2271
+ toolResults.push({ toolCallId, content: `Blocked by tool firewall: ${firewall.reason}` });
2272
+ return false;
2273
+ }
2274
+ } else {
2275
+ this.logger.warn(`Tool blocked by firewall (ask without handler): ${toolName}`, { reason: firewall.reason });
2276
+ toolResults.push({ toolCallId, content: `Blocked by tool firewall: ${firewall.reason}` });
2277
+ return false;
2278
+ }
2279
+ }
2280
+ return true;
2281
+ }
2282
+ /**
2283
+ * Executes a single parsed tool call and returns the serialised result string.
2284
+ */
2285
+ async executeSingleToolCall(tc) {
2286
+ this.checkExecutionBudget(tc.name);
2287
+ const args = JSON.parse(tc.arguments);
2288
+ const executeContext = {
2289
+ sessionId: "default",
2290
+ turnIndex: this.context.turns.length,
2291
+ logger: this.logger,
2292
+ adapter: this.adapter,
2293
+ stmRegistry: this.config.stmRegistry,
2294
+ scratchpad: this.context.scratchpad
2295
+ };
2296
+ if (this.config.ragAdapter) {
2297
+ executeContext["ragAdapter"] = this.config.ragAdapter;
2298
+ }
2299
+ this.logger.debug(`Executing tool: ${tc.name}`, { args: tc.arguments });
2300
+ this.emitTrace("tool_call", tc.name, { id: tc.id }, tc.arguments, null, "running");
2301
+ const result = await this.toolRegistry.execute(tc.name, args, executeContext);
2302
+ this.recordToolCall(tc.name);
2303
+ this.logger.debug(`Tool ${tc.name} returned successfully`);
2304
+ let finalResult = result;
2305
+ if (typeof result === "object" && result !== null) {
2306
+ const resObj = result;
2307
+ if (resObj["status"] === "success" && resObj["newScratchpad"] !== void 0) {
2308
+ this.context.scratchpad = resObj["newScratchpad"];
2309
+ finalResult = resObj["note"] || "Scratchpad updated";
2310
+ this.emitTrace("planning", "scratchpad_update", { note: resObj["note"] });
2311
+ }
2312
+ }
2313
+ let content = JSON.stringify(finalResult);
2314
+ const tokenCount = this.adapter.estimateTokens(content);
2315
+ if (this.config.maxTokensPerTool && tokenCount > this.config.maxTokensPerTool) {
2316
+ content = content.slice(0, this.config.maxTokensPerTool * 4) + "... [TRUNCATED DUE TO TOOL TOKEN LIMIT]";
2317
+ }
2318
+ const toolDef = this.toolRegistry.get(tc.name) || {
2319
+ name: tc.name,
2320
+ description: "",
2321
+ parameters: {},
2322
+ execute: async () => void 0
2323
+ };
2324
+ const evaluation = this.toolResponseProcessor.evaluate(content, toolDef, this.context);
2325
+ if (evaluation.shouldCompress && !evaluation.errorDetected) {
2326
+ content = this.toolResponseProcessor.compress(content, evaluation);
2327
+ this.emitTrace("compression", tc.name, { originalSize: evaluation.sizeClass }, null, content);
2328
+ }
2329
+ if (this.config.enableContinuationPlanning && evaluation.suggestedAction === "continue") {
2330
+ this.emitTrace("planning", "continuation_detected", { toolName: tc.name, action: evaluation.suggestedAction });
2331
+ }
2332
+ this.emitTrace("tool_result", tc.name, { id: tc.id }, tc.arguments, content, "done");
2333
+ return content;
2334
+ }
2335
+ // -----------------------------------------------------------------------
2336
+ // Public API
2337
+ // -----------------------------------------------------------------------
2338
+ /**
2339
+ * Runs the full ReAct loop for a user message and returns the final assistant response.
2340
+ *
2341
+ * @param userMessage - The user's message (string or multimodal content blocks)
2342
+ * @returns The assistant's final response string
2343
+ * @throws {LemuraMaxIterationsError} When the loop exceeds `maxIterations`
2344
+ */
2345
+ async run(userMessage) {
2346
+ if (this.mcpReady) await this.mcpReady;
2347
+ const userMessageStr = Array.isArray(userMessage) ? "[Multimodal Content]" : userMessage;
2348
+ this.logger.info(`Starting new session run`, {
2349
+ model: this.config.model,
2350
+ message: userMessageStr
2351
+ });
2352
+ const systemPrompt = this.buildSystemPrompt(userMessageStr);
2353
+ this.context.turns.push({
2354
+ role: "user",
2355
+ content: userMessage,
2356
+ tokenCount: Array.isArray(userMessage) ? userMessage.length * 50 : this.adapter.estimateTokens(userMessage),
2357
+ turnIndex: this.context.turns.length,
2358
+ compressed: false
2359
+ });
2360
+ const maxIts = this.config.maxIterations || 10;
2361
+ this.iterations = 0;
2362
+ this.stepCounter = new StepCounter(this.config.maxSteps ?? 20);
2363
+ while (this.iterations < maxIts) {
2364
+ this.iterations++;
2365
+ this.logger.debug(`ReAct Iteration ${this.iterations}/${maxIts}`);
2366
+ this.context = await this.contextManager.prepare(this.context);
2367
+ let messages = this.buildMessages(systemPrompt);
2368
+ if (this.stepCounter.isMaxReached()) {
2369
+ this.logger.warn(`maxSteps (${this.config.maxSteps ?? 20}) reached \u2014 forcing final response`);
2370
+ messages.push({
2371
+ role: "system",
2372
+ content: this.stepCounter.getForcedConclusionPrompt() + "\n\n" + FinalResponseFormatter.getRequiredStructure()
2373
+ });
2374
+ this.emitTrace("planning", "max_steps_reached", { maxSteps: this.config.maxSteps, currentSteps: this.stepCounter.count });
1110
2375
  }
1111
2376
  this.logger.debug(`Calling provider adapter (${this.adapter.name})...`);
2377
+ this.emitTrace("thinking", "llm_call", {
2378
+ model: this.config.model,
2379
+ iteration: this.iterations,
2380
+ totalTokens: this.totalTokens
2381
+ }, null, null, "running");
1112
2382
  let response;
1113
2383
  try {
1114
2384
  response = await this.adapter.complete({
1115
2385
  model: this.config.model,
1116
2386
  messages,
1117
- tools: this.toolRegistry.getAll(),
2387
+ tools: this.stepCounter.isMaxReached() ? [] : this.toolRegistry.getAll(),
1118
2388
  maxTokens: 1e3
1119
- // default gen tokens
1120
2389
  });
1121
2390
  } catch (err) {
1122
- const metadata = err.problem ? { problem: err.problem, hints: err.hints } : {};
1123
- this.logger.fatal(`Provider call failed: ${err.message}`, metadata);
2391
+ const e = err;
2392
+ const metadata = e.problem ? { problem: e.problem, hints: e.hints ?? [] } : {};
2393
+ this.logger.fatal(`Provider call failed: ${e.message ?? String(err)}`, metadata);
2394
+ this.emitTrace("error", "llm_call_failed", { error: e.message ?? String(err) });
1124
2395
  throw err;
1125
2396
  }
2397
+ if (response.usage) {
2398
+ this.totalTokens += response.usage.totalTokens;
2399
+ }
2400
+ this.emitTrace("thinking", "llm_call", {
2401
+ model: this.config.model,
2402
+ usage: response.usage,
2403
+ totalTokens: this.totalTokens
2404
+ }, null, response.content, "done");
1126
2405
  if (response.finishReason === "tool_call" && response.toolCalls) {
1127
2406
  this.logger.info(`Assistant requested ${response.toolCalls.length} tool calls`, {
1128
2407
  tools: response.toolCalls.map((tc) => tc.name)
1129
2408
  });
2409
+ this.stepCounter.increment(response.toolCalls.length);
1130
2410
  const toolResults = [];
1131
- for (const tc of response.toolCalls) {
1132
- try {
1133
- const args = JSON.parse(tc.arguments);
1134
- const executeContext = {
1135
- sessionId: "default",
1136
- turnIndex: this.context.turns.length,
1137
- logger: this.logger,
1138
- stmRegistry: this.config.stmRegistry,
1139
- scratchpad: this.context.scratchpad
1140
- };
1141
- if (this.config.ragAdapter) {
1142
- executeContext.ragAdapter = this.config.ragAdapter;
1143
- }
1144
- this.logger.debug(`Executing tool: ${tc.name}`, { args: tc.arguments });
1145
- const result = await this.toolRegistry.execute(tc.name, args, executeContext);
1146
- this.logger.debug(`Tool ${tc.name} returned successfully`);
1147
- let finalResult = result;
1148
- if (typeof result === "object" && result !== null) {
1149
- const resObj = result;
1150
- if (resObj.status === "success" && resObj.newScratchpad !== void 0) {
1151
- this.context.scratchpad = resObj.newScratchpad;
1152
- finalResult = resObj.note || "Scratchpad updated";
1153
- }
1154
- }
1155
- let content = JSON.stringify(finalResult);
1156
- const tokenCount = this.adapter.estimateTokens(content);
1157
- if (this.config.maxTokensPerTool && tokenCount > this.config.maxTokensPerTool) {
1158
- content = content.slice(0, this.config.maxTokensPerTool * 4) + "... [TRUNCATED DUE TO TOOL TOKEN LIMIT]";
2411
+ if (this.config.parallelToolCalls) {
2412
+ const budget = this.config.toolExecutionBudget;
2413
+ const maxConcurrent = budget?.maxConcurrentCalls ?? response.toolCalls.length;
2414
+ for (let i = 0; i < response.toolCalls.length; i += maxConcurrent) {
2415
+ const batch = response.toolCalls.slice(i, i + maxConcurrent);
2416
+ const allowed = [];
2417
+ for (const tc of batch) {
2418
+ const ok = await this.passesFirewall(tc.name, tc.arguments, tc.id, toolResults);
2419
+ if (ok) allowed.push(tc);
2420
+ else this.emitTrace("budget", "firewall_blocked", { toolName: tc.name });
1159
2421
  }
1160
- toolResults.push({ toolCallId: tc.id, content });
1161
- } catch (e) {
1162
- this.logger.error(`Tool ${tc.name} execution failed: ${e.message}`, {
1163
- problem: e.problem || `Tool ${tc.name} failed to execute properly.`,
1164
- hints: e.hints || ["Check the tool parameters and ensure the required services are running."]
2422
+ this.emitTrace("planning", "parallel_execution", {
2423
+ batchSize: allowed.length,
2424
+ totalInResponse: response.toolCalls.length
1165
2425
  });
1166
- toolResults.push({ toolCallId: tc.id, content: `Error: ${e.message}` });
2426
+ const batchResults = await Promise.all(
2427
+ allowed.map(async (tc) => {
2428
+ try {
2429
+ const content = await this.executeSingleToolCall(tc);
2430
+ return { toolCallId: tc.id, content };
2431
+ } catch (e) {
2432
+ const msg = e instanceof Error ? e.message : String(e);
2433
+ const isTimeout = e instanceof LemuraToolTimeoutError;
2434
+ this.logger.error(`Tool ${tc.name} ${isTimeout ? "timed out" : "failed"}: ${msg}`);
2435
+ return { toolCallId: tc.id, content: `Error: ${msg}` };
2436
+ }
2437
+ })
2438
+ );
2439
+ toolResults.push(...batchResults);
2440
+ }
2441
+ } else {
2442
+ for (const tc of response.toolCalls) {
2443
+ const ok = await this.passesFirewall(tc.name, tc.arguments, tc.id, toolResults);
2444
+ if (!ok) continue;
2445
+ try {
2446
+ const content = await this.executeSingleToolCall(tc);
2447
+ toolResults.push({ toolCallId: tc.id, content });
2448
+ } catch (e) {
2449
+ const msg = e instanceof Error ? e.message : String(e);
2450
+ const isTimeout = e instanceof LemuraToolTimeoutError;
2451
+ this.logger.error(`Tool ${tc.name} ${isTimeout ? "timed out" : "failed"}: ${msg}`, {
2452
+ problem: `Tool ${tc.name} ${isTimeout ? "timed out" : "failed to execute"}.`,
2453
+ hints: isTimeout ? ["Increase toolRegistryTimeoutMs or optimise the tool implementation."] : ["Check the tool parameters and ensure required services are running."]
2454
+ });
2455
+ toolResults.push({ toolCallId: tc.id, content: `Error: ${msg}` });
2456
+ }
1167
2457
  }
1168
2458
  }
1169
2459
  const assistantTurn = {
1170
2460
  role: "assistant",
1171
2461
  content: response.content || "",
1172
2462
  tokenCount: this.adapter.estimateTokens(response.content || "") + 50,
1173
- // rough tool estimate
1174
2463
  turnIndex: this.context.turns.length,
1175
2464
  compressed: false,
1176
2465
  toolCalls: response.toolCalls
@@ -1208,61 +2497,194 @@ var SessionManager = class {
1208
2497
  const maxItsErr = new LemuraMaxIterationsError(`Exceeded max iterations of ${maxIts}`);
1209
2498
  this.logger.fatal(maxItsErr.message, {
1210
2499
  problem: "The agent entered an infinite loop or took too many steps to resolve the task.",
1211
- hints: ["Increase maxIterations if the task is complex.", "Check if tools are returning consistent results."]
2500
+ hints: [
2501
+ "Increase maxIterations if the task is complex.",
2502
+ "Check if tools are returning consistent results."
2503
+ ]
1212
2504
  });
1213
2505
  throw maxItsErr;
1214
2506
  }
1215
- };
1216
-
1217
- // src/agent/execution/ToolResponseProcessor.ts
1218
- var ToolResponseProcessor = class {
1219
- evaluate(response, tool, context) {
1220
- const length = response.length;
1221
- const estimatedTokens = length / 4;
1222
- let sizeClass = "small";
1223
- if (estimatedTokens > 2e3) sizeClass = "oversized";
1224
- else if (estimatedTokens > 800) sizeClass = "large";
1225
- else if (estimatedTokens > 200) sizeClass = "medium";
1226
- return {
1227
- relevanceScore: 1,
1228
- sizeClass,
1229
- shouldCompress: sizeClass === "large" || sizeClass === "oversized",
1230
- suggestedMaxTokens: 500,
1231
- answered: true,
1232
- answeredPartially: false,
1233
- errorDetected: response.toLowerCase().includes("error"),
1234
- suggestedAction: "continue"
1235
- };
1236
- }
1237
- compress(response, evaluation) {
1238
- if (!evaluation.shouldCompress || evaluation.errorDetected) {
1239
- return response;
1240
- }
1241
- if (response.length > 1e3) {
1242
- return response.substring(0, 1e3) + "\n...[COMPRESSED TO SAVE TOKENS]...";
2507
+ /**
2508
+ * Runs the ReAct loop and streams the final assistant response token-by-token.
2509
+ *
2510
+ * Tool calls within the loop are still executed synchronously (they must complete
2511
+ * before streaming the conclusion). Only the final LLM text output is streamed.
2512
+ *
2513
+ * @param userMessage - The user's message (string or multimodal content blocks)
2514
+ * @returns An `AsyncIterable<string>` of delta tokens from the final response
2515
+ *
2516
+ * @example
2517
+ * for await (const token of session.stream('Tell me a story')) {
2518
+ * process.stdout.write(token);
2519
+ * }
2520
+ */
2521
+ async *stream(userMessage) {
2522
+ if (this.mcpReady) await this.mcpReady;
2523
+ const userMessageStr = Array.isArray(userMessage) ? "[Multimodal Content]" : userMessage;
2524
+ this.logger.info(`Starting streaming session run`, {
2525
+ model: this.config.model,
2526
+ message: userMessageStr
2527
+ });
2528
+ const systemPrompt = this.buildSystemPrompt(userMessageStr);
2529
+ this.context.turns.push({
2530
+ role: "user",
2531
+ content: userMessage,
2532
+ tokenCount: Array.isArray(userMessage) ? userMessage.length * 50 : this.adapter.estimateTokens(userMessage),
2533
+ turnIndex: this.context.turns.length,
2534
+ compressed: false
2535
+ });
2536
+ const maxIts = this.config.maxIterations || 10;
2537
+ this.iterations = 0;
2538
+ this.stepCounter = new StepCounter(this.config.maxSteps ?? 20);
2539
+ while (this.iterations < maxIts) {
2540
+ this.iterations++;
2541
+ this.logger.debug(`[stream] ReAct Iteration ${this.iterations}/${maxIts}`);
2542
+ this.context = await this.contextManager.prepare(this.context);
2543
+ const messages = this.buildMessages(systemPrompt);
2544
+ if (this.stepCounter.isMaxReached()) {
2545
+ messages.push({
2546
+ role: "system",
2547
+ content: this.stepCounter.getForcedConclusionPrompt() + "\n\n" + FinalResponseFormatter.getRequiredStructure()
2548
+ });
2549
+ }
2550
+ let response;
2551
+ try {
2552
+ response = await this.adapter.complete({
2553
+ model: this.config.model,
2554
+ messages,
2555
+ tools: this.stepCounter.isMaxReached() ? [] : this.toolRegistry.getAll(),
2556
+ maxTokens: 1e3
2557
+ });
2558
+ } catch (err) {
2559
+ this.logger.fatal(`Provider call failed: ${err.message}`);
2560
+ throw err;
2561
+ }
2562
+ if (response.finishReason === "tool_call" && response.toolCalls) {
2563
+ this.logger.info(`[stream] Tool calls: ${response.toolCalls.map((tc) => tc.name).join(", ")}`);
2564
+ this.stepCounter.increment(response.toolCalls.length);
2565
+ const toolResults = [];
2566
+ for (const tc of response.toolCalls) {
2567
+ const ok = await this.passesFirewall(tc.name, tc.arguments, tc.id, toolResults);
2568
+ if (!ok) continue;
2569
+ try {
2570
+ const content = await this.executeSingleToolCall(tc);
2571
+ toolResults.push({ toolCallId: tc.id, content });
2572
+ } catch (e) {
2573
+ const msg = e instanceof Error ? e.message : String(e);
2574
+ toolResults.push({ toolCallId: tc.id, content: `Error: ${msg}` });
2575
+ }
2576
+ }
2577
+ const assistantTurn = {
2578
+ role: "assistant",
2579
+ content: response.content || "",
2580
+ tokenCount: this.adapter.estimateTokens(response.content || "") + 50,
2581
+ turnIndex: this.context.turns.length,
2582
+ compressed: false,
2583
+ toolCalls: response.toolCalls
2584
+ };
2585
+ this.context.turns.push(assistantTurn);
2586
+ if (this.config.onTurn) this.config.onTurn(assistantTurn);
2587
+ for (const res of toolResults) {
2588
+ const toolTurn = {
2589
+ role: "tool",
2590
+ content: res.content,
2591
+ tokenCount: this.adapter.estimateTokens(res.content),
2592
+ turnIndex: this.context.turns.length,
2593
+ compressed: false,
2594
+ toolResults: [res]
2595
+ };
2596
+ this.context.turns.push(toolTurn);
2597
+ if (this.config.onTurn) this.config.onTurn(toolTurn);
2598
+ }
2599
+ continue;
2600
+ }
2601
+ this.context = await this.contextManager.prepare(this.context);
2602
+ const finalMessages = this.buildMessages(systemPrompt);
2603
+ let accumulated = "";
2604
+ let finalFinishReason = void 0;
2605
+ let finalTokenCount = 0;
2606
+ for await (const chunk of this.adapter.stream({
2607
+ model: this.config.model,
2608
+ messages: finalMessages,
2609
+ maxTokens: 1e3,
2610
+ stream: true
2611
+ })) {
2612
+ if (chunk.delta) {
2613
+ accumulated += chunk.delta;
2614
+ finalTokenCount += Math.ceil(chunk.delta.length / 4);
2615
+ yield chunk.delta;
2616
+ }
2617
+ if (chunk.finished) {
2618
+ finalFinishReason = chunk.finishReason;
2619
+ if (chunk.usage) {
2620
+ this.totalTokens += chunk.usage.totalTokens;
2621
+ }
2622
+ this.emitTrace("thinking", "llm_stream_finished", {
2623
+ usage: chunk.usage,
2624
+ totalTokens: this.totalTokens,
2625
+ finishReason: chunk.finishReason
2626
+ });
2627
+ }
2628
+ }
2629
+ const finalTurn = {
2630
+ role: "assistant",
2631
+ content: accumulated,
2632
+ tokenCount: finalTokenCount,
2633
+ turnIndex: this.context.turns.length,
2634
+ compressed: false
2635
+ };
2636
+ this.context.turns.push(finalTurn);
2637
+ if (this.config.onTurn) this.config.onTurn(finalTurn);
2638
+ this.logger.info(`[stream] Streaming run completed (finishReason: ${finalFinishReason ?? "stop"})`);
2639
+ return;
1243
2640
  }
1244
- return response;
2641
+ const maxItsErr = new LemuraMaxIterationsError(`Exceeded max iterations of ${maxIts}`);
2642
+ this.logger.fatal(maxItsErr.message, {
2643
+ problem: "The streaming agent loop exceeded its max iterations.",
2644
+ hints: ["Increase maxIterations or reduce task complexity."]
2645
+ });
2646
+ throw maxItsErr;
1245
2647
  }
1246
- };
1247
-
1248
- // src/agent/execution/GoalInjector.ts
1249
- var GoalInjector = class {
1250
- constructor(goal) {
1251
- this.goal = goal;
2648
+ /**
2649
+ * Resets the session: clears conversation history, resets iteration counters,
2650
+ * and resets tool execution budgets. The adapter, config, and tools are retained.
2651
+ */
2652
+ reset() {
2653
+ this.context = {
2654
+ systemPrompt: this.config.systemPrompt || "",
2655
+ scratchpad: "",
2656
+ turns: [],
2657
+ tokenCount: 0,
2658
+ maxTokens: this.config.maxTokens,
2659
+ metadata: {}
2660
+ };
2661
+ this.iterations = 0;
2662
+ this.totalToolCallCount = 0;
2663
+ this.perToolCallCount.clear();
2664
+ this.stepCounter = new StepCounter(this.config.maxSteps ?? 20);
2665
+ this.goalInjector = null;
2666
+ this.logger.debug("Session reset");
1252
2667
  }
1253
- injectInto(prompt) {
1254
- const formatting = `[CURRENT GOAL]
1255
- ${this.goal.statement}
1256
-
1257
- Success criteria:
1258
- ${this.goal.successCriteria.map((c) => `- ${c}`).join("\n")}
1259
- [/CURRENT GOAL]`;
1260
- if (this.goal.injectionPosition === "system_prompt") {
1261
- return `${prompt}
1262
-
1263
- ${formatting}`;
2668
+ /**
2669
+ * Closes the session and disconnects all MCP servers.
2670
+ *
2671
+ * Call this when you are done with the session and have registered MCP servers,
2672
+ * to ensure child processes are terminated and HTTP connections are released.
2673
+ *
2674
+ * @example
2675
+ * const session = new SessionManager({ ..., mcpServers: [...] });
2676
+ * try {
2677
+ * await session.run('Hello');
2678
+ * } finally {
2679
+ * await session.close();
2680
+ * }
2681
+ */
2682
+ async close() {
2683
+ if (this.mcpRegistry) {
2684
+ await this.mcpRegistry.disconnectAll();
2685
+ this.emitTrace("system", "mcp_disconnected", {});
2686
+ this.logger.debug("All MCP servers disconnected");
1264
2687
  }
1265
- return prompt;
1266
2688
  }
1267
2689
  };
1268
2690
 
@@ -1297,54 +2719,6 @@ var ContinuationPlanner = class {
1297
2719
  }
1298
2720
  };
1299
2721
 
1300
- // src/agent/execution/StepCounter.ts
1301
- var StepCounter = class {
1302
- constructor(maxSteps = 20) {
1303
- this.maxSteps = maxSteps;
1304
- }
1305
- toolCallCount = 0;
1306
- increment(count = 1) {
1307
- this.toolCallCount += count;
1308
- }
1309
- get count() {
1310
- return this.toolCallCount;
1311
- }
1312
- isMaxReached() {
1313
- return this.toolCallCount >= this.maxSteps;
1314
- }
1315
- getForcedConclusionPrompt() {
1316
- return `You have used ${this.toolCallCount}/${this.maxSteps} steps. Provide your final response now.
1317
- Do not call any more tools. Use the required structure below.`;
1318
- }
1319
- };
1320
-
1321
- // src/agent/execution/FinalResponseFormatter.ts
1322
- var FinalResponseFormatter = class {
1323
- static getRequiredStructure() {
1324
- return `## Goal Status: [ACHIEVED | PARTIALLY_ACHIEVED | FAILED]
1325
-
1326
- ### What was accomplished
1327
- [Summary of completed work]
1328
-
1329
- ### Remaining tasks
1330
- [Bulleted list, or "None"]
1331
-
1332
- ### Failed steps
1333
- [Tool/step name + error context, or "None"]
1334
-
1335
- ### Result
1336
- [The actual answer or deliverable]`;
1337
- }
1338
- static validateStructure(response) {
1339
- if (!response.includes("## Goal Status:")) return false;
1340
- if (!response.includes("### What was accomplished")) return false;
1341
- if (!response.includes("### Remaining tasks")) return false;
1342
- if (!response.includes("### Failed steps")) return false;
1343
- if (!response.includes("### Result")) return false;
1344
- return true;
1345
- }
1346
- };
1347
-
1348
2722
  exports.ContextManager = ContextManager;
1349
2723
  exports.ContinuationPlanner = ContinuationPlanner;
1350
2724
  exports.DefaultLogger = DefaultLogger;
@@ -1356,12 +2730,18 @@ exports.InMemoryStorageAdapter = InMemoryStorageAdapter;
1356
2730
  exports.LemuraAdapterError = LemuraAdapterError;
1357
2731
  exports.LemuraContextOverflowError = LemuraContextOverflowError;
1358
2732
  exports.LemuraError = LemuraError;
2733
+ exports.LemuraMCPConnectionError = LemuraMCPConnectionError;
2734
+ exports.LemuraMCPError = LemuraMCPError;
2735
+ exports.LemuraMCPTimeoutError = LemuraMCPTimeoutError;
1359
2736
  exports.LemuraMaxIterationsError = LemuraMaxIterationsError;
1360
2737
  exports.LemuraSkillInjectionError = LemuraSkillInjectionError;
1361
2738
  exports.LemuraToolNotFoundError = LemuraToolNotFoundError;
1362
2739
  exports.LemuraToolTimeoutError = LemuraToolTimeoutError;
1363
2740
  exports.LemuraToolValidationError = LemuraToolValidationError;
1364
2741
  exports.LogLevel = LogLevel;
2742
+ exports.MCPClient = MCPClient;
2743
+ exports.MCPClientRegistry = MCPClientRegistry;
2744
+ exports.MediaBridge = MediaBridge;
1365
2745
  exports.OpenAICompatibleAdapter = OpenAICompatibleAdapter;
1366
2746
  exports.SandwichCompressionStrategy = SandwichCompressionStrategy;
1367
2747
  exports.ScratchpadStrategy = ScratchpadStrategy;
@@ -1371,5 +2751,7 @@ exports.SkillInjector = SkillInjector;
1371
2751
  exports.StepCounter = StepCounter;
1372
2752
  exports.ToolRegistry = ToolRegistry;
1373
2753
  exports.ToolResponseProcessor = ToolResponseProcessor;
2754
+ exports.evaluateToolFirewall = evaluateToolFirewall;
2755
+ exports.validateJsonSchema = validateJsonSchema;
1374
2756
  //# sourceMappingURL=index.js.map
1375
2757
  //# sourceMappingURL=index.js.map