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