lemura 1.2.0 → 1.4.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/CHANGELOG.md +124 -0
- package/README.md +12 -11
- package/dist/adapters/index.d.mts +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +10 -2
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/index.mjs +10 -2
- package/dist/adapters/index.mjs.map +1 -1
- package/dist/{storage-CG3nTa6o.d.mts → adapters-BhTAnrOM.d.mts} +79 -46
- package/dist/{storage-DBt_q0wO.d.ts → adapters-CVcfWf85.d.ts} +79 -46
- package/dist/{agent-DxRd93wl.d.ts → agent-38El9_yp.d.ts} +35 -4
- package/dist/{agent-D6uhF-CZ.d.mts → agent-CJvmcAbT.d.mts} +35 -4
- package/dist/context/index.d.mts +71 -7
- package/dist/context/index.d.ts +71 -7
- package/dist/context/index.js +75 -4
- package/dist/context/index.js.map +1 -1
- package/dist/context/index.mjs +74 -5
- package/dist/context/index.mjs.map +1 -1
- package/dist/index.d.mts +339 -63
- package/dist/index.d.ts +339 -63
- package/dist/index.js +1018 -132
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1017 -133
- package/dist/index.mjs.map +1 -1
- package/dist/skills/index.d.mts +92 -3
- package/dist/skills/index.d.ts +92 -3
- package/dist/skills/index.js +138 -8
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/index.mjs +138 -8
- package/dist/skills/index.mjs.map +1 -1
- package/dist/skills-Y6D7zSSw.d.mts +66 -0
- package/dist/skills-Y6D7zSSw.d.ts +66 -0
- package/dist/tools/index.d.mts +3 -3
- package/dist/tools/index.d.ts +3 -3
- package/dist/types/index.d.mts +3 -3
- package/dist/types/index.d.ts +3 -3
- package/package.json +6 -1
- package/dist/skills-wc8S-OvC.d.mts +0 -14
- package/dist/skills-wc8S-OvC.d.ts +0 -14
package/dist/index.mjs
CHANGED
|
@@ -424,9 +424,17 @@ var OpenAICompatibleAdapter = class {
|
|
|
424
424
|
})
|
|
425
425
|
});
|
|
426
426
|
const data = await response.json();
|
|
427
|
+
if (data?.error) {
|
|
428
|
+
throw new LemuraAdapterError(data.error.message || "Image generation failed", "IMAGE_GEN_ERROR");
|
|
429
|
+
}
|
|
430
|
+
const first = data?.data?.[0] || {};
|
|
431
|
+
const imageUrl = first.url || (first.b64_json ? `data:image/png;base64,${first.b64_json}` : null) || (first.image_url?.url || null);
|
|
432
|
+
if (!imageUrl) {
|
|
433
|
+
throw new LemuraAdapterError("Image generation returned no image URL or base64 payload", "IMAGE_GEN_EMPTY");
|
|
434
|
+
}
|
|
427
435
|
return {
|
|
428
|
-
imageUrl
|
|
429
|
-
revisedPrompt:
|
|
436
|
+
imageUrl,
|
|
437
|
+
revisedPrompt: first.revised_prompt
|
|
430
438
|
};
|
|
431
439
|
}
|
|
432
440
|
};
|
|
@@ -454,6 +462,8 @@ var ContextManager = class {
|
|
|
454
462
|
*/
|
|
455
463
|
async prepare(context, safetyMargin = 0.95) {
|
|
456
464
|
let currentCtx = { ...context, turns: [...context.turns] };
|
|
465
|
+
const systemTokens = currentCtx.systemPrompt ? Math.ceil(currentCtx.systemPrompt.length / 4) : 0;
|
|
466
|
+
currentCtx.tokenCount = currentCtx.turns.reduce((sum, t) => sum + t.tokenCount, 0) + systemTokens;
|
|
457
467
|
currentCtx.maxTokens * safetyMargin;
|
|
458
468
|
for (const strategy of this.strategies) {
|
|
459
469
|
if (strategy.shouldApply(currentCtx)) {
|
|
@@ -474,11 +484,13 @@ var SandwichCompressionStrategy = class {
|
|
|
474
484
|
constructor(adapter, config) {
|
|
475
485
|
this.adapter = adapter;
|
|
476
486
|
this.config = config;
|
|
487
|
+
this.priority = config.priority ?? 20;
|
|
477
488
|
}
|
|
478
489
|
name = "sandwich_compression";
|
|
479
|
-
priority
|
|
490
|
+
priority;
|
|
480
491
|
shouldApply(ctx) {
|
|
481
|
-
|
|
492
|
+
const threshold = this.config.triggerThreshold ?? 0.8;
|
|
493
|
+
return ctx.tokenCount >= ctx.maxTokens * threshold && ctx.turns.length > this.config.preserveFirst + this.config.preserveLast;
|
|
482
494
|
}
|
|
483
495
|
async apply(ctx) {
|
|
484
496
|
const { preserveFirst, preserveLast } = this.config;
|
|
@@ -492,7 +504,8 @@ var SandwichCompressionStrategy = class {
|
|
|
492
504
|
role: "user",
|
|
493
505
|
content: `Summarize the following conversation history briefly:
|
|
494
506
|
${middleText}`
|
|
495
|
-
}]
|
|
507
|
+
}],
|
|
508
|
+
...this.config.summaryMaxTokens ? { maxTokens: this.config.summaryMaxTokens } : {}
|
|
496
509
|
});
|
|
497
510
|
const summaryStr = summaryResponse.content;
|
|
498
511
|
const newCompressionSummary = ctx.compressionSummary ? `${ctx.compressionSummary}
|
|
@@ -560,9 +573,10 @@ var HistoryCompressionStrategy = class {
|
|
|
560
573
|
constructor(adapter, config) {
|
|
561
574
|
this.adapter = adapter;
|
|
562
575
|
this.config = config;
|
|
576
|
+
this.priority = config.priority ?? 30;
|
|
563
577
|
}
|
|
564
578
|
name = "history_compression";
|
|
565
|
-
priority
|
|
579
|
+
priority;
|
|
566
580
|
shouldApply(ctx) {
|
|
567
581
|
const triggerTokens = ctx.maxTokens * this.config.triggerAtPercent;
|
|
568
582
|
const uncompressedTurns = ctx.turns.filter((t) => t.role !== "system" && !t.compressed);
|
|
@@ -594,6 +608,52 @@ ${summaryStr}` : summaryStr;
|
|
|
594
608
|
};
|
|
595
609
|
}
|
|
596
610
|
};
|
|
611
|
+
|
|
612
|
+
// src/context/SummaryInjectionStrategy.ts
|
|
613
|
+
var SummaryInjectionStrategy = class {
|
|
614
|
+
name = "summary_injection";
|
|
615
|
+
priority;
|
|
616
|
+
label;
|
|
617
|
+
constructor(config = {}) {
|
|
618
|
+
this.priority = config.priority ?? 1;
|
|
619
|
+
this.label = config.label ?? "Earlier conversation summary";
|
|
620
|
+
}
|
|
621
|
+
shouldApply(ctx) {
|
|
622
|
+
return !!ctx.compressionSummary && ctx.compressionSummary.trim().length > 0;
|
|
623
|
+
}
|
|
624
|
+
async apply(ctx) {
|
|
625
|
+
const summaryContent = `[${this.label}]
|
|
626
|
+
${ctx.compressionSummary}`;
|
|
627
|
+
const summaryTokenCount = Math.ceil(summaryContent.length / 4);
|
|
628
|
+
const existingIndex = ctx.turns.findIndex((t) => t.compressed && t.role === "system" && t.turnIndex === -1);
|
|
629
|
+
let newTurns;
|
|
630
|
+
if (existingIndex !== -1) {
|
|
631
|
+
newTurns = ctx.turns.map((t, i) => {
|
|
632
|
+
if (i !== existingIndex) return t;
|
|
633
|
+
return {
|
|
634
|
+
...t,
|
|
635
|
+
content: summaryContent,
|
|
636
|
+
tokenCount: summaryTokenCount
|
|
637
|
+
};
|
|
638
|
+
});
|
|
639
|
+
} else {
|
|
640
|
+
const summaryTurn = {
|
|
641
|
+
role: "system",
|
|
642
|
+
content: summaryContent,
|
|
643
|
+
tokenCount: summaryTokenCount,
|
|
644
|
+
turnIndex: -1,
|
|
645
|
+
compressed: true
|
|
646
|
+
};
|
|
647
|
+
newTurns = [summaryTurn, ...ctx.turns];
|
|
648
|
+
}
|
|
649
|
+
const newTokenCount = newTurns.reduce((sum, t) => sum + t.tokenCount, 0);
|
|
650
|
+
return {
|
|
651
|
+
...ctx,
|
|
652
|
+
turns: newTurns,
|
|
653
|
+
tokenCount: newTokenCount
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
};
|
|
597
657
|
var ShortTermMemoryRegistry = class {
|
|
598
658
|
storage;
|
|
599
659
|
maxTextTokens;
|
|
@@ -716,6 +776,23 @@ var InMemoryStorageAdapter = class {
|
|
|
716
776
|
}
|
|
717
777
|
};
|
|
718
778
|
|
|
779
|
+
// src/context/InMemoryScratchpadAdapter.ts
|
|
780
|
+
var InMemoryScratchpadAdapter = class {
|
|
781
|
+
store = /* @__PURE__ */ new Map();
|
|
782
|
+
async read(sessionId) {
|
|
783
|
+
return this.store.get(sessionId);
|
|
784
|
+
}
|
|
785
|
+
async write(sessionId, content) {
|
|
786
|
+
this.store.set(sessionId, content);
|
|
787
|
+
}
|
|
788
|
+
async clear(sessionId) {
|
|
789
|
+
this.store.delete(sessionId);
|
|
790
|
+
}
|
|
791
|
+
async healthCheck() {
|
|
792
|
+
return true;
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
|
|
719
796
|
// src/tools/SchemaValidator.ts
|
|
720
797
|
function typeOf(value) {
|
|
721
798
|
if (value === null) return "null";
|
|
@@ -1053,35 +1130,165 @@ var MediaBridge = class {
|
|
|
1053
1130
|
var SkillInjector = class {
|
|
1054
1131
|
skills = [];
|
|
1055
1132
|
constructor(skills = []) {
|
|
1056
|
-
this.skills =
|
|
1133
|
+
this.skills = skills.map((s) => this._normalise(s));
|
|
1057
1134
|
this.sortSkills();
|
|
1058
1135
|
}
|
|
1136
|
+
// -----------------------------------------------------------------------
|
|
1137
|
+
// Registration
|
|
1138
|
+
// -----------------------------------------------------------------------
|
|
1059
1139
|
register(skill) {
|
|
1060
|
-
this.skills.push(skill);
|
|
1140
|
+
this.skills.push(this._normalise(skill));
|
|
1061
1141
|
this.sortSkills();
|
|
1062
1142
|
}
|
|
1063
1143
|
sortSkills() {
|
|
1064
1144
|
this.skills.sort((a, b) => a.priority - b.priority);
|
|
1065
1145
|
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Normalises a skill to ensure consistent defaults.
|
|
1148
|
+
* For dynamic skills, `enabled` defaults to `false` unless explicitly set.
|
|
1149
|
+
* For fixed skills (or those without a strategy), `enabled` is ignored.
|
|
1150
|
+
*/
|
|
1151
|
+
_normalise(skill) {
|
|
1152
|
+
const strategy = skill.strategy ?? "fixed";
|
|
1153
|
+
const enabled = strategy === "dynamic" ? skill.enabled ?? false : true;
|
|
1154
|
+
return { ...skill, strategy, enabled };
|
|
1155
|
+
}
|
|
1156
|
+
// -----------------------------------------------------------------------
|
|
1157
|
+
// Dynamic skill activation
|
|
1158
|
+
// -----------------------------------------------------------------------
|
|
1159
|
+
/**
|
|
1160
|
+
* Enable a dynamic skill by name.
|
|
1161
|
+
* Has no effect on fixed skills (they are always active).
|
|
1162
|
+
*/
|
|
1163
|
+
enableSkill(name) {
|
|
1164
|
+
const skill = this.skills.find((s) => s.name === name);
|
|
1165
|
+
if (skill && skill.strategy === "dynamic") {
|
|
1166
|
+
skill.enabled = true;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Disable a dynamic skill by name.
|
|
1171
|
+
* Has no effect on fixed skills.
|
|
1172
|
+
*/
|
|
1173
|
+
disableSkill(name) {
|
|
1174
|
+
const skill = this.skills.find((s) => s.name === name);
|
|
1175
|
+
if (skill && skill.strategy === "dynamic") {
|
|
1176
|
+
skill.enabled = false;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Enable all dynamic skills whose `tags` array intersects with `tags`.
|
|
1181
|
+
*/
|
|
1182
|
+
enableByTags(tags) {
|
|
1183
|
+
const tagSet = new Set(tags);
|
|
1184
|
+
for (const skill of this.skills) {
|
|
1185
|
+
if (skill.strategy === "dynamic" && skill.tags?.some((t) => tagSet.has(t))) {
|
|
1186
|
+
skill.enabled = true;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Disable all dynamic skills whose `tags` array intersects with `tags`.
|
|
1192
|
+
*/
|
|
1193
|
+
disableByTags(tags) {
|
|
1194
|
+
const tagSet = new Set(tags);
|
|
1195
|
+
for (const skill of this.skills) {
|
|
1196
|
+
if (skill.strategy === "dynamic" && skill.tags?.some((t) => tagSet.has(t))) {
|
|
1197
|
+
skill.enabled = false;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
// -----------------------------------------------------------------------
|
|
1202
|
+
// Queries
|
|
1203
|
+
// -----------------------------------------------------------------------
|
|
1204
|
+
/**
|
|
1205
|
+
* Returns all registered skills (fixed and dynamic, enabled or not).
|
|
1206
|
+
*/
|
|
1207
|
+
getAll() {
|
|
1208
|
+
return [...this.skills];
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Returns currently active skills — fixed skills always, dynamic skills
|
|
1212
|
+
* only when `enabled === true`.
|
|
1213
|
+
*/
|
|
1214
|
+
getActiveSkills() {
|
|
1215
|
+
return this.skills.filter((s) => this._isActive(s));
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Returns skills at a given injection position, active only.
|
|
1219
|
+
*/
|
|
1066
1220
|
getSkillsForInjection(position) {
|
|
1067
|
-
return this.skills.filter((s) => s.inject === position);
|
|
1221
|
+
return this.skills.filter((s) => s.inject === position && this._isActive(s));
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Returns the union of `requiredTools` from all currently active skills.
|
|
1225
|
+
* Use this to determine which tools the active skill set depends on.
|
|
1226
|
+
*
|
|
1227
|
+
* @since 1.4.0
|
|
1228
|
+
*/
|
|
1229
|
+
getRequiredTools() {
|
|
1230
|
+
const tools = /* @__PURE__ */ new Set();
|
|
1231
|
+
for (const skill of this.getActiveSkills()) {
|
|
1232
|
+
for (const t of skill.requiredTools ?? []) {
|
|
1233
|
+
tools.add(t);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return [...tools];
|
|
1237
|
+
}
|
|
1238
|
+
_isActive(skill) {
|
|
1239
|
+
return skill.strategy !== "dynamic" || skill.enabled === true;
|
|
1068
1240
|
}
|
|
1241
|
+
// -----------------------------------------------------------------------
|
|
1242
|
+
// Injection block builder
|
|
1243
|
+
// -----------------------------------------------------------------------
|
|
1069
1244
|
/**
|
|
1070
|
-
*
|
|
1245
|
+
* Builds the combined injection block for all active skills at the given position.
|
|
1246
|
+
*
|
|
1247
|
+
* @param position - Which injection point to target.
|
|
1248
|
+
* @param tokenBudget - Optional maximum token budget. Skills are added in priority
|
|
1249
|
+
* order until the budget would be exceeded. When `undefined`, all active skills
|
|
1250
|
+
* at the position are included.
|
|
1071
1251
|
*/
|
|
1072
|
-
buildInjectionBlock(position) {
|
|
1252
|
+
buildInjectionBlock(position, tokenBudget) {
|
|
1073
1253
|
const relevantSkills = this.getSkillsForInjection(position);
|
|
1074
1254
|
if (relevantSkills.length === 0) return "";
|
|
1075
1255
|
let block = "";
|
|
1256
|
+
let usedTokens = 0;
|
|
1076
1257
|
for (const skill of relevantSkills) {
|
|
1077
|
-
const content =
|
|
1078
|
-
|
|
1079
|
-
|
|
1258
|
+
const content = this._pickContent(skill, tokenBudget !== void 0);
|
|
1259
|
+
if (!content) continue;
|
|
1260
|
+
const tierLabel = skill.tier ?? "standard";
|
|
1261
|
+
const skillEntry = `
|
|
1262
|
+
[Skill: ${skill.name} (Tier: ${tierLabel})]
|
|
1080
1263
|
${content}
|
|
1081
1264
|
`;
|
|
1265
|
+
const skillTokens = Math.ceil(skillEntry.length / 4);
|
|
1266
|
+
if (tokenBudget !== void 0 && usedTokens + skillTokens > tokenBudget) {
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
block += skillEntry;
|
|
1270
|
+
usedTokens += skillTokens;
|
|
1082
1271
|
}
|
|
1083
1272
|
return block.trim();
|
|
1084
1273
|
}
|
|
1274
|
+
// -----------------------------------------------------------------------
|
|
1275
|
+
// Content picking
|
|
1276
|
+
// -----------------------------------------------------------------------
|
|
1277
|
+
/**
|
|
1278
|
+
* Picks the content variant to use for a skill.
|
|
1279
|
+
*
|
|
1280
|
+
* Resolution order when budget-aware (compact first):
|
|
1281
|
+
* nano → micro → standard → content → description
|
|
1282
|
+
*
|
|
1283
|
+
* Resolution order when not budget-aware (richest first):
|
|
1284
|
+
* standard → content → micro → nano → description
|
|
1285
|
+
*/
|
|
1286
|
+
_pickContent(skill, budgetAware) {
|
|
1287
|
+
if (budgetAware) {
|
|
1288
|
+
return skill.nano || skill.micro || skill.standard || skill.content || skill.description || "";
|
|
1289
|
+
}
|
|
1290
|
+
return skill.standard || skill.content || skill.micro || skill.nano || skill.description || "";
|
|
1291
|
+
}
|
|
1085
1292
|
};
|
|
1086
1293
|
|
|
1087
1294
|
// src/rag/InMemoryRAGAdapter.ts
|
|
@@ -1293,6 +1500,10 @@ var readScratchpadTool = {
|
|
|
1293
1500
|
description: "Reads the content from the current agent scratchpad.",
|
|
1294
1501
|
parameters: { type: "object", properties: {} },
|
|
1295
1502
|
async execute(_params, context) {
|
|
1503
|
+
if (context.scratchpadAdapter) {
|
|
1504
|
+
const stored = await context.scratchpadAdapter.read(context.sessionId);
|
|
1505
|
+
return stored ?? "";
|
|
1506
|
+
}
|
|
1296
1507
|
return context.scratchpad ?? "";
|
|
1297
1508
|
}
|
|
1298
1509
|
};
|
|
@@ -1308,12 +1519,20 @@ var writeScratchpadTool = {
|
|
|
1308
1519
|
required: ["content"]
|
|
1309
1520
|
},
|
|
1310
1521
|
async execute(params, context) {
|
|
1311
|
-
let
|
|
1522
|
+
let current = context.scratchpad ?? "";
|
|
1523
|
+
if (context.scratchpadAdapter) {
|
|
1524
|
+
const stored = await context.scratchpadAdapter.read(context.sessionId);
|
|
1525
|
+
if (stored !== void 0) current = stored;
|
|
1526
|
+
}
|
|
1527
|
+
let newScratchpad = current;
|
|
1312
1528
|
if (params.append) {
|
|
1313
1529
|
newScratchpad += (newScratchpad ? "\n" : "") + params.content;
|
|
1314
1530
|
} else {
|
|
1315
1531
|
newScratchpad = params.content;
|
|
1316
1532
|
}
|
|
1533
|
+
if (context.scratchpadAdapter) {
|
|
1534
|
+
await context.scratchpadAdapter.write(context.sessionId, newScratchpad);
|
|
1535
|
+
}
|
|
1317
1536
|
return { status: "success", newScratchpad, note: "Scratchpad updated" };
|
|
1318
1537
|
}
|
|
1319
1538
|
};
|
|
@@ -1321,7 +1540,10 @@ var removeScratchpadTool = {
|
|
|
1321
1540
|
name: "remove_scratchpad",
|
|
1322
1541
|
description: "Clears or removes content from the scratchpad.",
|
|
1323
1542
|
parameters: { type: "object", properties: {} },
|
|
1324
|
-
async execute(_params,
|
|
1543
|
+
async execute(_params, context) {
|
|
1544
|
+
if (context.scratchpadAdapter) {
|
|
1545
|
+
await context.scratchpadAdapter.clear(context.sessionId);
|
|
1546
|
+
}
|
|
1325
1547
|
return { status: "success", newScratchpad: "", note: "Scratchpad cleared" };
|
|
1326
1548
|
}
|
|
1327
1549
|
};
|
|
@@ -1502,30 +1724,63 @@ var FinalResponseFormatter = class {
|
|
|
1502
1724
|
|
|
1503
1725
|
// src/agent/execution/ToolResponseProcessor.ts
|
|
1504
1726
|
var ToolResponseProcessor = class {
|
|
1727
|
+
smallMax;
|
|
1728
|
+
mediumMax;
|
|
1729
|
+
largeMax;
|
|
1730
|
+
budgetPercent;
|
|
1731
|
+
constructor(config = {}) {
|
|
1732
|
+
this.smallMax = config.smallMaxTokens ?? 200;
|
|
1733
|
+
this.mediumMax = config.mediumMaxTokens ?? 800;
|
|
1734
|
+
this.largeMax = config.largeMaxTokens ?? 2e3;
|
|
1735
|
+
this.budgetPercent = config.budgetPercent;
|
|
1736
|
+
}
|
|
1505
1737
|
evaluate(response, tool, context) {
|
|
1506
|
-
const
|
|
1507
|
-
const estimatedTokens = length / 4;
|
|
1738
|
+
const estimatedTokens = response.length / 4;
|
|
1508
1739
|
let sizeClass = "small";
|
|
1509
|
-
if (estimatedTokens >
|
|
1510
|
-
else if (estimatedTokens >
|
|
1511
|
-
else if (estimatedTokens >
|
|
1740
|
+
if (estimatedTokens > this.largeMax) sizeClass = "oversized";
|
|
1741
|
+
else if (estimatedTokens > this.mediumMax) sizeClass = "large";
|
|
1742
|
+
else if (estimatedTokens > this.smallMax) sizeClass = "medium";
|
|
1743
|
+
const lc = response.toLowerCase();
|
|
1744
|
+
const errorDetected = lc.includes("error:") || lc.includes("exception:") || lc.includes('"error"') || lc.includes('"status":"error"') || lc.includes('"status": "error"') || lc.includes("failed:") || lc.includes("connection refused") || lc.includes("timed out");
|
|
1745
|
+
const suggestedAction = errorDetected ? "retry" : sizeClass === "oversized" || sizeClass === "large" ? "continue" : "continue";
|
|
1512
1746
|
return {
|
|
1513
1747
|
relevanceScore: 1,
|
|
1514
1748
|
sizeClass,
|
|
1515
1749
|
shouldCompress: sizeClass === "large" || sizeClass === "oversized",
|
|
1516
|
-
suggestedMaxTokens:
|
|
1517
|
-
answered:
|
|
1518
|
-
answeredPartially:
|
|
1519
|
-
errorDetected
|
|
1520
|
-
suggestedAction
|
|
1750
|
+
suggestedMaxTokens: this.mediumMax,
|
|
1751
|
+
answered: !errorDetected,
|
|
1752
|
+
answeredPartially: errorDetected,
|
|
1753
|
+
errorDetected,
|
|
1754
|
+
suggestedAction
|
|
1521
1755
|
};
|
|
1522
1756
|
}
|
|
1523
1757
|
compress(response, evaluation) {
|
|
1524
1758
|
if (!evaluation.shouldCompress || evaluation.errorDetected) {
|
|
1525
1759
|
return response;
|
|
1526
1760
|
}
|
|
1527
|
-
if (
|
|
1528
|
-
|
|
1761
|
+
if (evaluation.sizeClass === "oversized") {
|
|
1762
|
+
const headChars = this.largeMax * 4;
|
|
1763
|
+
const tailChars = this.mediumMax * 2;
|
|
1764
|
+
if (response.length > headChars + tailChars) {
|
|
1765
|
+
const skipped = response.length - headChars - tailChars;
|
|
1766
|
+
return response.slice(0, headChars) + `
|
|
1767
|
+
|
|
1768
|
+
...[${skipped} characters omitted \u2014 response too large]...
|
|
1769
|
+
|
|
1770
|
+
` + response.slice(-tailChars);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
if (evaluation.sizeClass === "large") {
|
|
1774
|
+
const lines = response.split("\n");
|
|
1775
|
+
const keepLines = Math.ceil(this.mediumMax / 10);
|
|
1776
|
+
if (lines.length > keepLines * 2) {
|
|
1777
|
+
const skipped = lines.length - keepLines * 2;
|
|
1778
|
+
return [
|
|
1779
|
+
...lines.slice(0, keepLines),
|
|
1780
|
+
`... [${skipped} lines omitted] ...`,
|
|
1781
|
+
...lines.slice(-keepLines)
|
|
1782
|
+
].join("\n");
|
|
1783
|
+
}
|
|
1529
1784
|
}
|
|
1530
1785
|
return response;
|
|
1531
1786
|
}
|
|
@@ -1533,25 +1788,262 @@ var ToolResponseProcessor = class {
|
|
|
1533
1788
|
|
|
1534
1789
|
// src/agent/execution/GoalInjector.ts
|
|
1535
1790
|
var GoalInjector = class {
|
|
1791
|
+
goal;
|
|
1792
|
+
turnsSinceInjection = 0;
|
|
1536
1793
|
constructor(goal) {
|
|
1537
|
-
this.goal =
|
|
1794
|
+
this.goal = {
|
|
1795
|
+
...goal,
|
|
1796
|
+
completedSubGoals: goal.completedSubGoals ?? []
|
|
1797
|
+
};
|
|
1538
1798
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1799
|
+
/**
|
|
1800
|
+
* Returns the formatted `[CURRENT GOAL]` block string — without caring about
|
|
1801
|
+
* where it will be placed. Callers decide whether to append to a system prompt
|
|
1802
|
+
* or push as a separate message.
|
|
1803
|
+
*/
|
|
1804
|
+
getFormattedBlock() {
|
|
1805
|
+
const { statement, successCriteria, decomposition, completedSubGoals = [] } = this.goal;
|
|
1806
|
+
const pending = decomposition.filter((sg) => !completedSubGoals.includes(sg));
|
|
1807
|
+
const completed = decomposition.filter((sg) => completedSubGoals.includes(sg));
|
|
1808
|
+
let block = `[CURRENT GOAL]
|
|
1809
|
+
${statement}
|
|
1810
|
+
`;
|
|
1811
|
+
if (successCriteria.length > 0) {
|
|
1812
|
+
block += `
|
|
1543
1813
|
Success criteria:
|
|
1544
|
-
${
|
|
1545
|
-
|
|
1546
|
-
if (
|
|
1547
|
-
|
|
1814
|
+
${successCriteria.map((c) => `- ${c}`).join("\n")}`;
|
|
1815
|
+
}
|
|
1816
|
+
if (pending.length > 0) {
|
|
1817
|
+
block += `
|
|
1548
1818
|
|
|
1549
|
-
|
|
1819
|
+
Sub-goals remaining:
|
|
1820
|
+
${pending.map((sg) => `- ${sg} \u2190 pending`).join("\n")}`;
|
|
1550
1821
|
}
|
|
1551
|
-
|
|
1822
|
+
if (completed.length > 0) {
|
|
1823
|
+
block += `
|
|
1824
|
+
|
|
1825
|
+
Sub-goals completed:
|
|
1826
|
+
${completed.map((sg) => `- \u2705 ${sg}`).join("\n")}`;
|
|
1827
|
+
}
|
|
1828
|
+
block += "\n[/CURRENT GOAL]";
|
|
1829
|
+
return block;
|
|
1552
1830
|
}
|
|
1831
|
+
/**
|
|
1832
|
+
* Appends the goal block to the given prompt string (for `system_prompt` position).
|
|
1833
|
+
* For `pre_turn` position, use `getFormattedBlock()` directly.
|
|
1834
|
+
*
|
|
1835
|
+
* @param prompt - The existing system prompt to append to.
|
|
1836
|
+
*/
|
|
1837
|
+
injectInto(prompt) {
|
|
1838
|
+
const block = this.getFormattedBlock();
|
|
1839
|
+
return prompt ? `${prompt}
|
|
1840
|
+
|
|
1841
|
+
${block}` : block;
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* Returns true when the goal should be re-injected this turn,
|
|
1845
|
+
* based on `injectionFrequency`.
|
|
1846
|
+
*
|
|
1847
|
+
* @param turnIndex - The current turn index in the ReAct loop (0-based)
|
|
1848
|
+
* @param compressionOccurred - Whether context was compressed this iteration
|
|
1849
|
+
* @param injectionN - The N for 'every_N_turns' frequency (default: 3)
|
|
1850
|
+
*/
|
|
1851
|
+
shouldInjectThisTurn(turnIndex, compressionOccurred = false, injectionN = 3) {
|
|
1852
|
+
const { injectionFrequency } = this.goal;
|
|
1853
|
+
if (injectionFrequency === "always") return true;
|
|
1854
|
+
if (injectionFrequency === "every_N_turns") {
|
|
1855
|
+
return turnIndex % injectionN === 0;
|
|
1856
|
+
}
|
|
1857
|
+
if (injectionFrequency === "on_compression") {
|
|
1858
|
+
return compressionOccurred;
|
|
1859
|
+
}
|
|
1860
|
+
return true;
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Updates the goal with new sub-goal decomposition and success criteria,
|
|
1864
|
+
* typically populated by the mini-planning LLM call.
|
|
1865
|
+
*/
|
|
1866
|
+
updateDecomposition(decomposition, successCriteria) {
|
|
1867
|
+
this.goal = {
|
|
1868
|
+
...this.goal,
|
|
1869
|
+
decomposition,
|
|
1870
|
+
...successCriteria ? { successCriteria } : {}
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
/**
|
|
1874
|
+
* Marks a sub-goal as completed so it moves to the "completed" section
|
|
1875
|
+
* in subsequent injections.
|
|
1876
|
+
*/
|
|
1877
|
+
markSubGoalDone(subGoal) {
|
|
1878
|
+
const completed = this.goal.completedSubGoals ?? [];
|
|
1879
|
+
if (!completed.includes(subGoal)) {
|
|
1880
|
+
this.goal = {
|
|
1881
|
+
...this.goal,
|
|
1882
|
+
completedSubGoals: [...completed, subGoal]
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
/** Returns a snapshot of the current goal state (safe to store in context.metadata). */
|
|
1553
1887
|
getGoal() {
|
|
1554
|
-
return { ...this.goal };
|
|
1888
|
+
return { ...this.goal, completedSubGoals: [...this.goal.completedSubGoals ?? []] };
|
|
1889
|
+
}
|
|
1890
|
+
/** Increments the internal turn counter (used for `every_N_turns` frequency). */
|
|
1891
|
+
incrementTurn() {
|
|
1892
|
+
this.turnsSinceInjection++;
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
|
|
1896
|
+
// src/agent/execution/ContinuationPlanner.ts
|
|
1897
|
+
var ContinuationPlanner = class {
|
|
1898
|
+
plan;
|
|
1899
|
+
outputs = /* @__PURE__ */ new Map();
|
|
1900
|
+
constructor(plan) {
|
|
1901
|
+
this.plan = { ...plan, steps: plan.steps.map((s) => ({ ...s })) };
|
|
1902
|
+
}
|
|
1903
|
+
// -------------------------------------------------------------------------
|
|
1904
|
+
// State queries
|
|
1905
|
+
// -------------------------------------------------------------------------
|
|
1906
|
+
/** Returns the current plan (deep copy) */
|
|
1907
|
+
getPlan() {
|
|
1908
|
+
return { ...this.plan, steps: this.plan.steps.map((s) => ({ ...s })) };
|
|
1909
|
+
}
|
|
1910
|
+
/** Returns a human-readable status string with icons (injected before each iteration) */
|
|
1911
|
+
getPlanStatusString() {
|
|
1912
|
+
let result = `[CONTINUATION PLAN \u2014 Step ${this.plan.currentStepIndex + 1}/${this.plan.steps.length}]
|
|
1913
|
+
`;
|
|
1914
|
+
for (const step of this.plan.steps) {
|
|
1915
|
+
const icon = this._icon(step.status);
|
|
1916
|
+
const statusText = step.status === "pending" && step.dependsOn.length > 0 ? `Waiting on Step ${step.dependsOn.join(", ")}` : step.status.charAt(0).toUpperCase() + step.status.slice(1);
|
|
1917
|
+
result += `${icon} Step ${step.stepId} (${step.toolName}): ${statusText}
|
|
1918
|
+
`;
|
|
1919
|
+
}
|
|
1920
|
+
return result;
|
|
1921
|
+
}
|
|
1922
|
+
_icon(status) {
|
|
1923
|
+
switch (status) {
|
|
1924
|
+
case "done":
|
|
1925
|
+
return "\u2705";
|
|
1926
|
+
case "running":
|
|
1927
|
+
return "\u25B6";
|
|
1928
|
+
case "failed":
|
|
1929
|
+
return "\u274C";
|
|
1930
|
+
case "skipped":
|
|
1931
|
+
return "\u23ED";
|
|
1932
|
+
default:
|
|
1933
|
+
return "\u23F3";
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
/** Returns all steps whose dependencies are satisfied and that are still pending */
|
|
1937
|
+
getReadySteps() {
|
|
1938
|
+
return this.plan.steps.filter((step) => {
|
|
1939
|
+
if (step.status !== "pending") return false;
|
|
1940
|
+
const depsOk = step.dependsOn.every((depId) => {
|
|
1941
|
+
const dep = this.plan.steps.find((s) => s.stepId === depId);
|
|
1942
|
+
return dep?.status === "done";
|
|
1943
|
+
});
|
|
1944
|
+
if (!depsOk) return false;
|
|
1945
|
+
if (step.condition) {
|
|
1946
|
+
const condDepOutput = this.outputs.get(step.condition.step) ?? "";
|
|
1947
|
+
if (!condDepOutput.includes(step.condition.outputContains)) {
|
|
1948
|
+
return false;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
return true;
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
/** Returns true when all steps have reached a terminal state */
|
|
1955
|
+
isComplete() {
|
|
1956
|
+
return this.plan.steps.every(
|
|
1957
|
+
(s) => s.status === "done" || s.status === "failed" || s.status === "skipped"
|
|
1958
|
+
);
|
|
1959
|
+
}
|
|
1960
|
+
// -------------------------------------------------------------------------
|
|
1961
|
+
// State mutations
|
|
1962
|
+
// -------------------------------------------------------------------------
|
|
1963
|
+
/** Marks a step as running */
|
|
1964
|
+
markStepRunning(stepId) {
|
|
1965
|
+
this._updateStep(stepId, { status: "running" });
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* Marks a step as done, stores its output under `outputKey` (if set),
|
|
1969
|
+
* and advances `currentStepIndex` to the next pending step.
|
|
1970
|
+
*/
|
|
1971
|
+
markStepDone(stepId, output) {
|
|
1972
|
+
const step = this.plan.steps.find((s) => s.stepId === stepId);
|
|
1973
|
+
if (step?.outputKey && output !== void 0) {
|
|
1974
|
+
this.outputs.set(step.outputKey, output);
|
|
1975
|
+
}
|
|
1976
|
+
this._updateStep(stepId, { status: "done" });
|
|
1977
|
+
this._advanceIndex();
|
|
1978
|
+
}
|
|
1979
|
+
/**
|
|
1980
|
+
* Marks a step as failed and propagates `skipped` to all transitively dependent steps.
|
|
1981
|
+
*/
|
|
1982
|
+
markStepFailed(stepId) {
|
|
1983
|
+
this._updateStep(stepId, { status: "failed" });
|
|
1984
|
+
this._skipDependants(stepId);
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* Marks a step as skipped (e.g., condition not met) and propagates to its dependants.
|
|
1988
|
+
*/
|
|
1989
|
+
markStepSkipped(stepId) {
|
|
1990
|
+
this._updateStep(stepId, { status: "skipped" });
|
|
1991
|
+
this._skipDependants(stepId);
|
|
1992
|
+
}
|
|
1993
|
+
// -------------------------------------------------------------------------
|
|
1994
|
+
// Output / input mapping helpers
|
|
1995
|
+
// -------------------------------------------------------------------------
|
|
1996
|
+
/** Retrieves an output stored by `outputKey` from a completed step */
|
|
1997
|
+
getOutput(key) {
|
|
1998
|
+
return this.outputs.get(key);
|
|
1999
|
+
}
|
|
2000
|
+
/**
|
|
2001
|
+
* Resolves the `inputMapping` for a step into a concrete parameter map,
|
|
2002
|
+
* substituting `outputKey` values from prior steps. Static (non-key) values
|
|
2003
|
+
* in the mapping are passed through unchanged.
|
|
2004
|
+
*/
|
|
2005
|
+
resolveInputs(step, baseArgs = {}) {
|
|
2006
|
+
if (!step.inputMapping) return baseArgs;
|
|
2007
|
+
const resolved = { ...baseArgs };
|
|
2008
|
+
for (const [paramName, outputKey] of Object.entries(step.inputMapping)) {
|
|
2009
|
+
const value = this.outputs.get(outputKey);
|
|
2010
|
+
if (value !== void 0) {
|
|
2011
|
+
resolved[paramName] = value;
|
|
2012
|
+
} else {
|
|
2013
|
+
resolved[paramName] = outputKey;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
return resolved;
|
|
2017
|
+
}
|
|
2018
|
+
// -------------------------------------------------------------------------
|
|
2019
|
+
// Internals
|
|
2020
|
+
// -------------------------------------------------------------------------
|
|
2021
|
+
_updateStep(stepId, patch) {
|
|
2022
|
+
this.plan.steps = this.plan.steps.map(
|
|
2023
|
+
(s) => s.stepId === stepId ? { ...s, ...patch } : s
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
_skipDependants(failedStepId) {
|
|
2027
|
+
const toSkip = /* @__PURE__ */ new Set([failedStepId]);
|
|
2028
|
+
let changed = true;
|
|
2029
|
+
while (changed) {
|
|
2030
|
+
changed = false;
|
|
2031
|
+
for (const step of this.plan.steps) {
|
|
2032
|
+
if (step.status === "pending" && step.dependsOn.some((d) => toSkip.has(d))) {
|
|
2033
|
+
this._updateStep(step.stepId, { status: "skipped" });
|
|
2034
|
+
toSkip.add(step.stepId);
|
|
2035
|
+
changed = true;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
_advanceIndex() {
|
|
2041
|
+
const nextPending = this.plan.steps.findIndex(
|
|
2042
|
+
(s, i) => i > this.plan.currentStepIndex && s.status === "pending"
|
|
2043
|
+
);
|
|
2044
|
+
if (nextPending !== -1) {
|
|
2045
|
+
this.plan.currentStepIndex = nextPending;
|
|
2046
|
+
}
|
|
1555
2047
|
}
|
|
1556
2048
|
};
|
|
1557
2049
|
var MCPClient = class {
|
|
@@ -2024,6 +2516,9 @@ var SessionManager = class {
|
|
|
2024
2516
|
iterations = 0;
|
|
2025
2517
|
logger;
|
|
2026
2518
|
media;
|
|
2519
|
+
sessionId;
|
|
2520
|
+
scratchpadLoaded = false;
|
|
2521
|
+
pendingScratchpadClear = false;
|
|
2027
2522
|
// Advanced execution state
|
|
2028
2523
|
stepCounter;
|
|
2029
2524
|
toolResponseProcessor;
|
|
@@ -2031,7 +2526,7 @@ var SessionManager = class {
|
|
|
2031
2526
|
continuationPlanner = null;
|
|
2032
2527
|
// MCP
|
|
2033
2528
|
mcpRegistry = null;
|
|
2034
|
-
/** Resolves when all MCP servers are connected;
|
|
2529
|
+
/** Resolves when all MCP servers are connected; awaited by run() and stream() */
|
|
2035
2530
|
mcpReady = null;
|
|
2036
2531
|
// Tool execution budget tracking
|
|
2037
2532
|
totalToolCallCount = 0;
|
|
@@ -2041,14 +2536,23 @@ var SessionManager = class {
|
|
|
2041
2536
|
this.config = config;
|
|
2042
2537
|
this.adapter = config.adapter;
|
|
2043
2538
|
this.logger = config.logger || new DefaultLogger();
|
|
2539
|
+
this.sessionId = config.sessionId || "default";
|
|
2044
2540
|
this.contextManager = new ContextManager();
|
|
2045
2541
|
this.toolRegistry = new ToolRegistry(config.tools || [], {
|
|
2046
2542
|
defaultTimeoutMs: config.toolRegistryTimeoutMs ?? 3e4
|
|
2047
2543
|
});
|
|
2048
2544
|
this.skillInjector = new SkillInjector(config.skills || []);
|
|
2545
|
+
if (config.activeDynamicSkills && config.activeDynamicSkills.length > 0) {
|
|
2546
|
+
for (const name of config.activeDynamicSkills) {
|
|
2547
|
+
this.skillInjector.enableSkill(name);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
if (config.activeDynamicTags && config.activeDynamicTags.length > 0) {
|
|
2551
|
+
this.skillInjector.enableByTags(config.activeDynamicTags);
|
|
2552
|
+
}
|
|
2049
2553
|
this.media = new MediaBridge(this.adapter);
|
|
2050
2554
|
this.stepCounter = new StepCounter(config.maxSteps ?? 20);
|
|
2051
|
-
this.toolResponseProcessor = config.toolResponseProcessor
|
|
2555
|
+
this.toolResponseProcessor = config.toolResponseProcessor instanceof ToolResponseProcessor ? config.toolResponseProcessor : config.toolResponseProcessor ? config.toolResponseProcessor : new ToolResponseProcessor();
|
|
2052
2556
|
for (const strategy of config.compressionStrategies || []) {
|
|
2053
2557
|
this.contextManager.registerStrategy(strategy);
|
|
2054
2558
|
}
|
|
@@ -2076,11 +2580,11 @@ var SessionManager = class {
|
|
|
2076
2580
|
maxTokens: config.maxTokens,
|
|
2077
2581
|
metadata: {}
|
|
2078
2582
|
};
|
|
2079
|
-
this.goalInjector = null;
|
|
2080
2583
|
if (config.mcpServers && config.mcpServers.length > 0) {
|
|
2081
2584
|
this.mcpRegistry = new MCPClientRegistry(this.logger);
|
|
2082
2585
|
this.mcpReady = this._initMCP(config.mcpServers);
|
|
2083
2586
|
}
|
|
2587
|
+
const activeSkills = this.skillInjector.getActiveSkills();
|
|
2084
2588
|
this.emitTrace("system", "session_init", {
|
|
2085
2589
|
config: {
|
|
2086
2590
|
model: this.config.model,
|
|
@@ -2089,9 +2593,29 @@ var SessionManager = class {
|
|
|
2089
2593
|
parallelToolCalls: this.config.parallelToolCalls,
|
|
2090
2594
|
enableGoalPlanning: this.config.enableGoalPlanning,
|
|
2091
2595
|
enableContinuationPlanning: this.config.enableContinuationPlanning
|
|
2596
|
+
},
|
|
2597
|
+
skills: {
|
|
2598
|
+
total: (config.skills || []).length,
|
|
2599
|
+
active: activeSkills.length,
|
|
2600
|
+
fixed: activeSkills.filter((s) => s.strategy !== "dynamic").length,
|
|
2601
|
+
dynamic: activeSkills.filter((s) => s.strategy === "dynamic").length
|
|
2092
2602
|
}
|
|
2093
2603
|
});
|
|
2604
|
+
for (const skill of activeSkills) {
|
|
2605
|
+
this.emitTrace("skill", "skill_load", {
|
|
2606
|
+
name: skill.name,
|
|
2607
|
+
version: skill.version,
|
|
2608
|
+
strategy: skill.strategy ?? "fixed",
|
|
2609
|
+
inject: skill.inject,
|
|
2610
|
+
priority: skill.priority,
|
|
2611
|
+
tags: skill.tags ?? [],
|
|
2612
|
+
requiredTools: skill.requiredTools ?? []
|
|
2613
|
+
});
|
|
2614
|
+
}
|
|
2094
2615
|
}
|
|
2616
|
+
// -----------------------------------------------------------------------
|
|
2617
|
+
// Trace helper
|
|
2618
|
+
// -----------------------------------------------------------------------
|
|
2095
2619
|
emitTrace(type, name, metadata, input, output, status = "done") {
|
|
2096
2620
|
if (this.config.onTrace) {
|
|
2097
2621
|
this.config.onTrace({
|
|
@@ -2105,24 +2629,172 @@ var SessionManager = class {
|
|
|
2105
2629
|
});
|
|
2106
2630
|
}
|
|
2107
2631
|
}
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2632
|
+
async ensureScratchpadLoaded() {
|
|
2633
|
+
if (!this.config.scratchpadAdapter) return;
|
|
2634
|
+
if (this.pendingScratchpadClear) {
|
|
2635
|
+
await this.config.scratchpadAdapter.clear(this.sessionId);
|
|
2636
|
+
this.context.scratchpad = "";
|
|
2637
|
+
this.pendingScratchpadClear = false;
|
|
2638
|
+
this.scratchpadLoaded = true;
|
|
2639
|
+
return;
|
|
2640
|
+
}
|
|
2641
|
+
if (this.scratchpadLoaded) return;
|
|
2642
|
+
const stored = await this.config.scratchpadAdapter.read(this.sessionId);
|
|
2643
|
+
this.context.scratchpad = stored ?? "";
|
|
2644
|
+
this.scratchpadLoaded = true;
|
|
2645
|
+
}
|
|
2646
|
+
// -----------------------------------------------------------------------
|
|
2647
|
+
// Public accessors
|
|
2648
|
+
// -----------------------------------------------------------------------
|
|
2649
|
+
/** Returns a shallow copy of the current context window. */
|
|
2111
2650
|
getContext() {
|
|
2112
2651
|
return { ...this.context };
|
|
2113
2652
|
}
|
|
2114
|
-
/**
|
|
2115
|
-
* Returns the current conversation history.
|
|
2116
|
-
*/
|
|
2653
|
+
/** Returns the current conversation history. */
|
|
2117
2654
|
getHistory() {
|
|
2118
2655
|
return [...this.context.turns];
|
|
2119
2656
|
}
|
|
2120
2657
|
/**
|
|
2121
|
-
*
|
|
2658
|
+
* Populates the session context with a pre-existing conversation history.
|
|
2659
|
+
*
|
|
2660
|
+
* Turns are assigned sequential `turnIndex` values starting from 0 and the
|
|
2661
|
+
* context `tokenCount` is recalculated automatically. Call this immediately
|
|
2662
|
+
* after construction and before the first `run()` / `stream()`.
|
|
2663
|
+
*
|
|
2664
|
+
* @param history - Raw history entries (role + content, optional toolCalls/toolResults).
|
|
2665
|
+
*
|
|
2666
|
+
* @example
|
|
2667
|
+
* ```typescript
|
|
2668
|
+
* const session = new SessionManager(config);
|
|
2669
|
+
* session.loadHistory(savedMessages);
|
|
2670
|
+
* const answer = await session.run('Continue where we left off.');
|
|
2671
|
+
* ```
|
|
2122
2672
|
*/
|
|
2673
|
+
loadHistory(history) {
|
|
2674
|
+
this.context.turns = history.map((m, i) => ({
|
|
2675
|
+
role: m.role,
|
|
2676
|
+
content: m.content ?? "",
|
|
2677
|
+
tokenCount: this.adapter.estimateTokens(
|
|
2678
|
+
typeof m.content === "string" ? m.content : JSON.stringify(m.content)
|
|
2679
|
+
),
|
|
2680
|
+
turnIndex: i,
|
|
2681
|
+
compressed: false,
|
|
2682
|
+
...m.toolCalls ? { toolCalls: m.toolCalls } : {},
|
|
2683
|
+
...m.toolResults ? { toolResults: m.toolResults } : {}
|
|
2684
|
+
}));
|
|
2685
|
+
this.context.tokenCount = this.context.turns.reduce((sum, t) => sum + t.tokenCount, 0) + this.adapter.estimateTokens(this.context.systemPrompt || "");
|
|
2686
|
+
this.emitTrace("system", "history_loaded", { turnCount: this.context.turns.length });
|
|
2687
|
+
}
|
|
2688
|
+
/** Returns the `MediaBridge` for direct ASR / TTS / Vision / Image-gen calls. */
|
|
2123
2689
|
getMedia() {
|
|
2124
2690
|
return this.media;
|
|
2125
2691
|
}
|
|
2692
|
+
/**
|
|
2693
|
+
* Returns the `ToolRegistry` for runtime tool management.
|
|
2694
|
+
*
|
|
2695
|
+
* Use this to register or unregister tools after session construction:
|
|
2696
|
+
*
|
|
2697
|
+
* ```typescript
|
|
2698
|
+
* // Add a tool after the user authenticates
|
|
2699
|
+
* session.tools.register(paymentTool);
|
|
2700
|
+
*
|
|
2701
|
+
* // Remove a tool when no longer needed
|
|
2702
|
+
* session.tools.unregister('send_payment');
|
|
2703
|
+
*
|
|
2704
|
+
* // Inspect what's registered
|
|
2705
|
+
* const active = session.tools.getAll();
|
|
2706
|
+
* console.log(active.map(t => t.name));
|
|
2707
|
+
* ```
|
|
2708
|
+
*
|
|
2709
|
+
* @since 1.4.0
|
|
2710
|
+
*/
|
|
2711
|
+
get tools() {
|
|
2712
|
+
return this.toolRegistry;
|
|
2713
|
+
}
|
|
2714
|
+
/**
|
|
2715
|
+
* Returns the `SkillInjector` for runtime skill management.
|
|
2716
|
+
*
|
|
2717
|
+
* Use this to enable or disable dynamic skills after session construction:
|
|
2718
|
+
*
|
|
2719
|
+
* ```typescript
|
|
2720
|
+
* session.skills.enableSkill('code-review');
|
|
2721
|
+
* session.skills.enableByTags(['debugging']);
|
|
2722
|
+
* session.skills.disableSkill('verbose-mode');
|
|
2723
|
+
* const requiredTools = session.skills.getRequiredTools();
|
|
2724
|
+
* ```
|
|
2725
|
+
*
|
|
2726
|
+
* @since 1.4.0
|
|
2727
|
+
*/
|
|
2728
|
+
get skills() {
|
|
2729
|
+
return this.skillInjector;
|
|
2730
|
+
}
|
|
2731
|
+
// -----------------------------------------------------------------------
|
|
2732
|
+
// Continuation planning API
|
|
2733
|
+
// -----------------------------------------------------------------------
|
|
2734
|
+
/**
|
|
2735
|
+
* Sets an explicit multi-step continuation plan that will be tracked and
|
|
2736
|
+
* injected as a status block before each ReAct iteration.
|
|
2737
|
+
*
|
|
2738
|
+
* Dependency tracking, condition evaluation, `outputKey` storage, and
|
|
2739
|
+
* `inputMapping` resolution are all handled automatically by the planner.
|
|
2740
|
+
*
|
|
2741
|
+
* @param steps - The ordered list of continuation steps.
|
|
2742
|
+
* @param strategy - Execution strategy ('sequential' | 'parallel' | 'conditional'). Default: 'sequential'.
|
|
2743
|
+
*
|
|
2744
|
+
* @example
|
|
2745
|
+
* ```typescript
|
|
2746
|
+
* await session.setPlan([
|
|
2747
|
+
* { stepId: 'fetch', toolName: 'fetch_data', description: 'Get data', dependsOn: [], outputKey: 'rawData' },
|
|
2748
|
+
* { stepId: 'analyze', toolName: 'analyze', description: 'Analyze', dependsOn: ['fetch'], inputMapping: { data: 'rawData' } },
|
|
2749
|
+
* ]);
|
|
2750
|
+
* const result = await session.run('Run the data pipeline.');
|
|
2751
|
+
* ```
|
|
2752
|
+
*/
|
|
2753
|
+
setPlan(steps, strategy = "sequential") {
|
|
2754
|
+
this.continuationPlanner = new ContinuationPlanner({
|
|
2755
|
+
steps,
|
|
2756
|
+
currentStepIndex: 0,
|
|
2757
|
+
strategy
|
|
2758
|
+
});
|
|
2759
|
+
this.context.metadata["continuationPlan"] = this.continuationPlanner.getPlan();
|
|
2760
|
+
this.logger.debug(`[ContinuationPlanner] Plan set with ${steps.length} steps (strategy: ${strategy})`);
|
|
2761
|
+
this.emitTrace("planning", "plan_set", { stepCount: steps.length, strategy });
|
|
2762
|
+
}
|
|
2763
|
+
// -----------------------------------------------------------------------
|
|
2764
|
+
// Goal planning API
|
|
2765
|
+
// -----------------------------------------------------------------------
|
|
2766
|
+
/**
|
|
2767
|
+
* Manually sets the agent's goal, bypassing the automatic mini-planning LLM call.
|
|
2768
|
+
*
|
|
2769
|
+
* Use this when you already know the goal structure upfront.
|
|
2770
|
+
*
|
|
2771
|
+
* @example
|
|
2772
|
+
* ```typescript
|
|
2773
|
+
* await session.setGoal({
|
|
2774
|
+
* statement: 'Audit the authentication module',
|
|
2775
|
+
* decomposition: ['Read src/auth/', 'Identify SQL injection risks', 'Write report'],
|
|
2776
|
+
* successCriteria: ['Report covers all audit areas', 'Each finding has a severity rating'],
|
|
2777
|
+
* });
|
|
2778
|
+
* const result = await session.run('Begin the security audit.');
|
|
2779
|
+
* ```
|
|
2780
|
+
*/
|
|
2781
|
+
setGoal(goal) {
|
|
2782
|
+
this.goalInjector = new GoalInjector({
|
|
2783
|
+
id: "manual",
|
|
2784
|
+
statement: goal.statement,
|
|
2785
|
+
decomposition: goal.decomposition ?? [],
|
|
2786
|
+
successCriteria: goal.successCriteria ?? [],
|
|
2787
|
+
injectionFrequency: this.config.goalInjectionFrequency ?? "always",
|
|
2788
|
+
injectionPosition: this.config.goalInjectionPosition ?? "system_prompt",
|
|
2789
|
+
completedSubGoals: goal.completedSubGoals ?? []
|
|
2790
|
+
});
|
|
2791
|
+
this.context.metadata["goal"] = this.goalInjector.getGoal();
|
|
2792
|
+
this.logger.debug("[GoalInjector] Goal set manually");
|
|
2793
|
+
this.emitTrace("planning", "goal_set_manual", {
|
|
2794
|
+
statement: goal.statement,
|
|
2795
|
+
subGoals: goal.decomposition?.length ?? 0
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2126
2798
|
// -----------------------------------------------------------------------
|
|
2127
2799
|
// MCP initialisation
|
|
2128
2800
|
// -----------------------------------------------------------------------
|
|
@@ -2160,41 +2832,83 @@ var SessionManager = class {
|
|
|
2160
2832
|
});
|
|
2161
2833
|
}
|
|
2162
2834
|
// -----------------------------------------------------------------------
|
|
2835
|
+
// Goal mini-planning step (one extra LLM call, gated by enableGoalPlanning)
|
|
2836
|
+
// -----------------------------------------------------------------------
|
|
2837
|
+
/**
|
|
2838
|
+
* Runs a dedicated planning prompt against the LLM to decompose the user's
|
|
2839
|
+
* message into sub-goals and success criteria. Called once at the start of
|
|
2840
|
+
* the first `run()` when `enableGoalPlanning` is true and no goal has been
|
|
2841
|
+
* manually set via `setGoal()`.
|
|
2842
|
+
*/
|
|
2843
|
+
async _runMiniPlanningStep(userMessage) {
|
|
2844
|
+
const planningPrompt = `Given this goal: "${userMessage}"
|
|
2845
|
+
|
|
2846
|
+
1. List the sub-goals needed to achieve this (max 5, be specific)
|
|
2847
|
+
2. List success criteria \u2014 what does "done" look like? (max 3, binary, measurable)
|
|
2848
|
+
|
|
2849
|
+
Respond ONLY with valid JSON (no markdown, no explanations):
|
|
2850
|
+
{ "subGoals": string[], "successCriteria": string[] }`;
|
|
2851
|
+
try {
|
|
2852
|
+
const response = await this.adapter.complete({
|
|
2853
|
+
model: this.config.model,
|
|
2854
|
+
messages: [{ role: "user", content: planningPrompt }],
|
|
2855
|
+
maxTokens: this.config.maxCompletionTokens ?? 2e3
|
|
2856
|
+
});
|
|
2857
|
+
const raw = response.content.replace(/```json|```/g, "").trim();
|
|
2858
|
+
const parsed = JSON.parse(raw);
|
|
2859
|
+
if (this.goalInjector && Array.isArray(parsed.subGoals)) {
|
|
2860
|
+
this.goalInjector.updateDecomposition(
|
|
2861
|
+
parsed.subGoals,
|
|
2862
|
+
Array.isArray(parsed.successCriteria) ? parsed.successCriteria : void 0
|
|
2863
|
+
);
|
|
2864
|
+
this.context.metadata["goal"] = this.goalInjector.getGoal();
|
|
2865
|
+
this.emitTrace("planning", "mini_plan_done", {
|
|
2866
|
+
subGoals: parsed.subGoals,
|
|
2867
|
+
successCriteria: parsed.successCriteria
|
|
2868
|
+
});
|
|
2869
|
+
this.logger.debug(`[GoalInjector] Mini-plan: ${parsed.subGoals.length} sub-goals`);
|
|
2870
|
+
}
|
|
2871
|
+
} catch (err) {
|
|
2872
|
+
this.logger.warn(`[GoalInjector] Mini-planning step failed: ${err.message ?? String(err)}`);
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
// -----------------------------------------------------------------------
|
|
2163
2876
|
// Internal helpers
|
|
2164
2877
|
// -----------------------------------------------------------------------
|
|
2165
2878
|
/** Builds the system prompt, injecting skills and goal if configured. */
|
|
2166
|
-
buildSystemPrompt(userMessage) {
|
|
2879
|
+
buildSystemPrompt(userMessage, iteration = 0) {
|
|
2167
2880
|
let prompt = this.context.systemPrompt || "";
|
|
2168
|
-
if (this.config.
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
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") {
|
|
2881
|
+
if (this.goalInjector && this.config.goalInjectionPosition !== "pre_turn") {
|
|
2882
|
+
const shouldInject = this.goalInjector.shouldInjectThisTurn(
|
|
2883
|
+
iteration,
|
|
2884
|
+
false,
|
|
2885
|
+
this.config.goalInjectionN ?? 3
|
|
2886
|
+
);
|
|
2887
|
+
if (shouldInject) {
|
|
2186
2888
|
prompt = this.goalInjector.injectInto(prompt);
|
|
2187
|
-
this.emitTrace("planning", "goal_injected", {
|
|
2889
|
+
this.emitTrace("planning", "goal_injected", {
|
|
2890
|
+
position: "system_prompt",
|
|
2891
|
+
iteration
|
|
2892
|
+
});
|
|
2188
2893
|
}
|
|
2189
2894
|
}
|
|
2190
|
-
|
|
2895
|
+
if (this.continuationPlanner && this.config.enableContinuationPlanning) {
|
|
2896
|
+
const planStatus = this.continuationPlanner.getPlanStatusString();
|
|
2897
|
+
prompt += `
|
|
2898
|
+
|
|
2899
|
+
${planStatus}`;
|
|
2900
|
+
}
|
|
2901
|
+
const injectedSkills = this.skillInjector.buildInjectionBlock(
|
|
2902
|
+
"system_prompt",
|
|
2903
|
+
this.config.skillTokenBudget
|
|
2904
|
+
);
|
|
2191
2905
|
if (injectedSkills) {
|
|
2192
2906
|
prompt += "\n\n" + injectedSkills;
|
|
2193
2907
|
}
|
|
2194
2908
|
return prompt.trim();
|
|
2195
2909
|
}
|
|
2196
|
-
/** Builds the messages array for the provider from the current context */
|
|
2197
|
-
buildMessages(systemPrompt) {
|
|
2910
|
+
/** Builds the messages array for the provider from the current context. */
|
|
2911
|
+
buildMessages(systemPrompt, iteration = 0) {
|
|
2198
2912
|
const messages = this.context.turns.map((t) => ({
|
|
2199
2913
|
role: t.role,
|
|
2200
2914
|
content: t.content,
|
|
@@ -2205,13 +2919,20 @@ var SessionManager = class {
|
|
|
2205
2919
|
messages.unshift({ role: "system", content: systemPrompt });
|
|
2206
2920
|
}
|
|
2207
2921
|
if (this.goalInjector && this.config.goalInjectionPosition === "pre_turn") {
|
|
2208
|
-
const
|
|
2209
|
-
|
|
2210
|
-
|
|
2922
|
+
const shouldInject = this.goalInjector.shouldInjectThisTurn(
|
|
2923
|
+
iteration,
|
|
2924
|
+
false,
|
|
2925
|
+
this.config.goalInjectionN ?? 3
|
|
2926
|
+
);
|
|
2927
|
+
if (shouldInject) {
|
|
2928
|
+
const goalBlock = this.goalInjector.getFormattedBlock();
|
|
2929
|
+
messages.push({ role: "system", content: goalBlock });
|
|
2930
|
+
this.emitTrace("planning", "goal_injected", { position: "pre_turn", iteration });
|
|
2931
|
+
}
|
|
2211
2932
|
}
|
|
2212
2933
|
return messages;
|
|
2213
2934
|
}
|
|
2214
|
-
/** Checks the tool execution budget and throws descriptively if exceeded */
|
|
2935
|
+
/** Checks the tool execution budget and throws descriptively if exceeded. */
|
|
2215
2936
|
checkExecutionBudget(toolName) {
|
|
2216
2937
|
const budget = this.config.toolExecutionBudget;
|
|
2217
2938
|
if (!budget) return;
|
|
@@ -2229,7 +2950,11 @@ var SessionManager = class {
|
|
|
2229
2950
|
`Tool execution budget exceeded: '${toolName}' has reached its per-tool limit of ${budget.maxCallsPerTool[toolName]}`
|
|
2230
2951
|
);
|
|
2231
2952
|
this.logger.warn(err.message);
|
|
2232
|
-
this.emitTrace("budget", "tool_limit_exceeded", {
|
|
2953
|
+
this.emitTrace("budget", "tool_limit_exceeded", {
|
|
2954
|
+
toolName,
|
|
2955
|
+
limit: budget.maxCallsPerTool[toolName],
|
|
2956
|
+
totalTokens: this.totalTokens
|
|
2957
|
+
});
|
|
2233
2958
|
throw err;
|
|
2234
2959
|
}
|
|
2235
2960
|
}
|
|
@@ -2240,11 +2965,80 @@ var SessionManager = class {
|
|
|
2240
2965
|
tokenBudgetRemaining: this.config.maxTokens - this.totalTokens
|
|
2241
2966
|
});
|
|
2242
2967
|
}
|
|
2243
|
-
/** Records a tool call in budget counters */
|
|
2968
|
+
/** Records a tool call in budget counters. */
|
|
2244
2969
|
recordToolCall(toolName) {
|
|
2245
2970
|
this.totalToolCallCount++;
|
|
2246
2971
|
this.perToolCallCount.set(toolName, (this.perToolCallCount.get(toolName) ?? 0) + 1);
|
|
2247
2972
|
}
|
|
2973
|
+
// -----------------------------------------------------------------------
|
|
2974
|
+
// Blob detection helpers
|
|
2975
|
+
// -----------------------------------------------------------------------
|
|
2976
|
+
isProbablyBase64(value) {
|
|
2977
|
+
const v = value.trim();
|
|
2978
|
+
if (v.startsWith("data:") && v.includes(";base64,")) return true;
|
|
2979
|
+
if (v.length < 4096) return false;
|
|
2980
|
+
if (/[^A-Za-z0-9+/=]/.test(v)) return false;
|
|
2981
|
+
return true;
|
|
2982
|
+
}
|
|
2983
|
+
isBinaryLike(value) {
|
|
2984
|
+
if (!value) return false;
|
|
2985
|
+
if (typeof ArrayBuffer !== "undefined") {
|
|
2986
|
+
if (value instanceof ArrayBuffer) return true;
|
|
2987
|
+
if (ArrayBuffer.isView(value)) return true;
|
|
2988
|
+
}
|
|
2989
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) return true;
|
|
2990
|
+
return false;
|
|
2991
|
+
}
|
|
2992
|
+
async storeBlob(content, metadata) {
|
|
2993
|
+
if (!this.config.stmRegistry) return null;
|
|
2994
|
+
return this.config.stmRegistry.register(content, "blob", metadata);
|
|
2995
|
+
}
|
|
2996
|
+
async scrubBlobFields(value, toolName, depth = 0) {
|
|
2997
|
+
if (!value || typeof value !== "object") return { value, changed: false };
|
|
2998
|
+
if (depth > 2) return { value, changed: false };
|
|
2999
|
+
if (Array.isArray(value)) {
|
|
3000
|
+
let changed2 = false;
|
|
3001
|
+
const out = [];
|
|
3002
|
+
for (const item of value) {
|
|
3003
|
+
const res = await this.scrubBlobFields(item, toolName, depth + 1);
|
|
3004
|
+
out.push(res.value);
|
|
3005
|
+
if (res.changed) changed2 = true;
|
|
3006
|
+
}
|
|
3007
|
+
return { value: out, changed: changed2 };
|
|
3008
|
+
}
|
|
3009
|
+
const obj = { ...value };
|
|
3010
|
+
let changed = false;
|
|
3011
|
+
for (const [key, v] of Object.entries(obj)) {
|
|
3012
|
+
if (typeof v === "string" && this.isProbablyBase64(v)) {
|
|
3013
|
+
const ref = await this.storeBlob(v, {
|
|
3014
|
+
toolName,
|
|
3015
|
+
key,
|
|
3016
|
+
encoding: v.startsWith("data:") ? "data_url" : "base64"
|
|
3017
|
+
});
|
|
3018
|
+
if (ref) {
|
|
3019
|
+
obj[key] = ref;
|
|
3020
|
+
obj[`${key}Note`] = "Stored in STM";
|
|
3021
|
+
changed = true;
|
|
3022
|
+
}
|
|
3023
|
+
continue;
|
|
3024
|
+
}
|
|
3025
|
+
if (this.isBinaryLike(v)) {
|
|
3026
|
+
const ref = await this.storeBlob(v, { toolName, key });
|
|
3027
|
+
if (ref) {
|
|
3028
|
+
obj[key] = ref;
|
|
3029
|
+
obj[`${key}Note`] = "Stored in STM";
|
|
3030
|
+
changed = true;
|
|
3031
|
+
}
|
|
3032
|
+
continue;
|
|
3033
|
+
}
|
|
3034
|
+
if (typeof v === "object" && v !== null) {
|
|
3035
|
+
const nested = await this.scrubBlobFields(v, toolName, depth + 1);
|
|
3036
|
+
obj[key] = nested.value;
|
|
3037
|
+
if (nested.changed) changed = true;
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
return { value: obj, changed };
|
|
3041
|
+
}
|
|
2248
3042
|
/**
|
|
2249
3043
|
* Processes a firewall decision for a tool call.
|
|
2250
3044
|
* Returns true to proceed, false to block.
|
|
@@ -2279,24 +3073,53 @@ var SessionManager = class {
|
|
|
2279
3073
|
}
|
|
2280
3074
|
/**
|
|
2281
3075
|
* Executes a single parsed tool call and returns the serialised result string.
|
|
3076
|
+
* Also handles continuation plan tracking (step status + output storage).
|
|
2282
3077
|
*/
|
|
2283
3078
|
async executeSingleToolCall(tc) {
|
|
2284
3079
|
this.checkExecutionBudget(tc.name);
|
|
2285
|
-
|
|
3080
|
+
let args = JSON.parse(tc.arguments);
|
|
3081
|
+
if (this.continuationPlanner) {
|
|
3082
|
+
const plan = this.continuationPlanner.getPlan();
|
|
3083
|
+
const matchingStep = plan.steps.find(
|
|
3084
|
+
(s) => s.toolName === tc.name && s.status === "pending"
|
|
3085
|
+
);
|
|
3086
|
+
if (matchingStep) {
|
|
3087
|
+
this.continuationPlanner.markStepRunning(matchingStep.stepId);
|
|
3088
|
+
args = this.continuationPlanner.resolveInputs(matchingStep, args);
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
2286
3091
|
const executeContext = {
|
|
2287
|
-
sessionId:
|
|
3092
|
+
sessionId: this.sessionId,
|
|
2288
3093
|
turnIndex: this.context.turns.length,
|
|
2289
3094
|
logger: this.logger,
|
|
2290
3095
|
adapter: this.adapter,
|
|
2291
3096
|
stmRegistry: this.config.stmRegistry,
|
|
2292
|
-
scratchpad: this.context.scratchpad
|
|
3097
|
+
scratchpad: this.context.scratchpad,
|
|
3098
|
+
scratchpadAdapter: this.config.scratchpadAdapter
|
|
2293
3099
|
};
|
|
2294
3100
|
if (this.config.ragAdapter) {
|
|
2295
3101
|
executeContext["ragAdapter"] = this.config.ragAdapter;
|
|
2296
3102
|
}
|
|
2297
|
-
this.logger.debug(`Executing tool: ${tc.name}`, { args:
|
|
2298
|
-
this.emitTrace("tool_call", tc.name, { id: tc.id },
|
|
2299
|
-
|
|
3103
|
+
this.logger.debug(`Executing tool: ${tc.name}`, { args: JSON.stringify(args) });
|
|
3104
|
+
this.emitTrace("tool_call", tc.name, { id: tc.id }, JSON.stringify(args), null, "running");
|
|
3105
|
+
let result;
|
|
3106
|
+
let executionError = null;
|
|
3107
|
+
try {
|
|
3108
|
+
result = await this.toolRegistry.execute(tc.name, args, executeContext);
|
|
3109
|
+
} catch (err) {
|
|
3110
|
+
executionError = err;
|
|
3111
|
+
if (this.continuationPlanner) {
|
|
3112
|
+
const plan = this.continuationPlanner.getPlan();
|
|
3113
|
+
const runningStep = plan.steps.find(
|
|
3114
|
+
(s) => s.toolName === tc.name && s.status === "running"
|
|
3115
|
+
);
|
|
3116
|
+
if (runningStep) {
|
|
3117
|
+
this.continuationPlanner.markStepFailed(runningStep.stepId);
|
|
3118
|
+
this.context.metadata["continuationPlan"] = this.continuationPlanner.getPlan();
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
throw executionError;
|
|
3122
|
+
}
|
|
2300
3123
|
this.recordToolCall(tc.name);
|
|
2301
3124
|
this.logger.debug(`Tool ${tc.name} returned successfully`);
|
|
2302
3125
|
let finalResult = result;
|
|
@@ -2308,6 +3131,22 @@ var SessionManager = class {
|
|
|
2308
3131
|
this.emitTrace("planning", "scratchpad_update", { note: resObj["note"] });
|
|
2309
3132
|
}
|
|
2310
3133
|
}
|
|
3134
|
+
if (this.config.stmRegistry) {
|
|
3135
|
+
if (typeof finalResult === "string" && this.isProbablyBase64(finalResult)) {
|
|
3136
|
+
const ref = await this.storeBlob(finalResult, { toolName: tc.name, encoding: "base64" });
|
|
3137
|
+
if (ref) {
|
|
3138
|
+
finalResult = { blobRef: ref, note: "Binary content stored in STM" };
|
|
3139
|
+
}
|
|
3140
|
+
} else if (this.isBinaryLike(finalResult)) {
|
|
3141
|
+
const ref = await this.storeBlob(finalResult, { toolName: tc.name });
|
|
3142
|
+
if (ref) {
|
|
3143
|
+
finalResult = { blobRef: ref, note: "Binary content stored in STM" };
|
|
3144
|
+
}
|
|
3145
|
+
} else if (typeof finalResult === "object" && finalResult !== null) {
|
|
3146
|
+
const scrubbed = await this.scrubBlobFields(finalResult, tc.name);
|
|
3147
|
+
if (scrubbed.changed) finalResult = scrubbed.value;
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
2311
3150
|
let content = JSON.stringify(finalResult);
|
|
2312
3151
|
const tokenCount = this.adapter.estimateTokens(content);
|
|
2313
3152
|
if (this.config.maxTokensPerTool && tokenCount > this.config.maxTokensPerTool) {
|
|
@@ -2324,10 +3163,32 @@ var SessionManager = class {
|
|
|
2324
3163
|
content = this.toolResponseProcessor.compress(content, evaluation);
|
|
2325
3164
|
this.emitTrace("compression", tc.name, { originalSize: evaluation.sizeClass }, null, content);
|
|
2326
3165
|
}
|
|
2327
|
-
if (this.
|
|
2328
|
-
|
|
3166
|
+
if (this.continuationPlanner) {
|
|
3167
|
+
const plan = this.continuationPlanner.getPlan();
|
|
3168
|
+
const runningStep = plan.steps.find(
|
|
3169
|
+
(s) => s.toolName === tc.name && s.status === "running"
|
|
3170
|
+
);
|
|
3171
|
+
if (runningStep) {
|
|
3172
|
+
this.continuationPlanner.markStepDone(runningStep.stepId, content);
|
|
3173
|
+
if (runningStep.outputKey) {
|
|
3174
|
+
const outputs = this.context.metadata["toolOutputs"] ?? {};
|
|
3175
|
+
outputs[runningStep.outputKey] = content;
|
|
3176
|
+
this.context.metadata["toolOutputs"] = outputs;
|
|
3177
|
+
}
|
|
3178
|
+
this.context.metadata["continuationPlan"] = this.continuationPlanner.getPlan();
|
|
3179
|
+
this.emitTrace("planning", "step_done", {
|
|
3180
|
+
stepId: runningStep.stepId,
|
|
3181
|
+
outputKey: runningStep.outputKey
|
|
3182
|
+
});
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
if (evaluation.suggestedAction === "continue" && this.config.enableContinuationPlanning) {
|
|
3186
|
+
this.emitTrace("planning", "continuation_detected", {
|
|
3187
|
+
toolName: tc.name,
|
|
3188
|
+
action: evaluation.suggestedAction
|
|
3189
|
+
});
|
|
2329
3190
|
}
|
|
2330
|
-
this.emitTrace("tool_result", tc.name, { id: tc.id },
|
|
3191
|
+
this.emitTrace("tool_result", tc.name, { id: tc.id }, JSON.stringify(args), content, "done");
|
|
2331
3192
|
return content;
|
|
2332
3193
|
}
|
|
2333
3194
|
// -----------------------------------------------------------------------
|
|
@@ -2336,18 +3197,38 @@ var SessionManager = class {
|
|
|
2336
3197
|
/**
|
|
2337
3198
|
* Runs the full ReAct loop for a user message and returns the final assistant response.
|
|
2338
3199
|
*
|
|
3200
|
+
* When `enableGoalPlanning` is true and no goal has been manually set, a mini-planning
|
|
3201
|
+
* LLM call is made before the first iteration to decompose the task into sub-goals.
|
|
3202
|
+
*
|
|
2339
3203
|
* @param userMessage - The user's message (string or multimodal content blocks)
|
|
2340
3204
|
* @returns The assistant's final response string
|
|
2341
3205
|
* @throws {LemuraMaxIterationsError} When the loop exceeds `maxIterations`
|
|
2342
3206
|
*/
|
|
2343
3207
|
async run(userMessage) {
|
|
2344
3208
|
if (this.mcpReady) await this.mcpReady;
|
|
3209
|
+
await this.ensureScratchpadLoaded();
|
|
2345
3210
|
const userMessageStr = Array.isArray(userMessage) ? "[Multimodal Content]" : userMessage;
|
|
2346
3211
|
this.logger.info(`Starting new session run`, {
|
|
2347
3212
|
model: this.config.model,
|
|
2348
3213
|
message: userMessageStr
|
|
2349
3214
|
});
|
|
2350
|
-
|
|
3215
|
+
if (this.config.enableGoalPlanning && !this.goalInjector) {
|
|
3216
|
+
this.goalInjector = new GoalInjector({
|
|
3217
|
+
id: "auto",
|
|
3218
|
+
statement: typeof userMessage === "string" ? userMessage : "[multimodal]",
|
|
3219
|
+
decomposition: [],
|
|
3220
|
+
successCriteria: ["The user request is fully answered"],
|
|
3221
|
+
injectionFrequency: this.config.goalInjectionFrequency ?? "always",
|
|
3222
|
+
injectionPosition: this.config.goalInjectionPosition ?? "system_prompt"
|
|
3223
|
+
});
|
|
3224
|
+
this.context.metadata["goal"] = this.goalInjector.getGoal();
|
|
3225
|
+
this.logger.debug("Goal injector initialised (auto)");
|
|
3226
|
+
this.emitTrace("planning", "goal_init", {
|
|
3227
|
+
statement: this.goalInjector.getGoal().statement,
|
|
3228
|
+
criteria: this.goalInjector.getGoal().successCriteria
|
|
3229
|
+
});
|
|
3230
|
+
await this._runMiniPlanningStep(userMessageStr);
|
|
3231
|
+
}
|
|
2351
3232
|
this.context.turns.push({
|
|
2352
3233
|
role: "user",
|
|
2353
3234
|
content: userMessage,
|
|
@@ -2358,18 +3239,24 @@ var SessionManager = class {
|
|
|
2358
3239
|
const maxIts = this.config.maxIterations || 10;
|
|
2359
3240
|
this.iterations = 0;
|
|
2360
3241
|
this.stepCounter = new StepCounter(this.config.maxSteps ?? 20);
|
|
3242
|
+
const maxCompletionTokens = this.config.maxCompletionTokens ?? 2e3;
|
|
2361
3243
|
while (this.iterations < maxIts) {
|
|
2362
3244
|
this.iterations++;
|
|
2363
3245
|
this.logger.debug(`ReAct Iteration ${this.iterations}/${maxIts}`);
|
|
3246
|
+
this.context.tokenCount = this.context.turns.reduce((sum, t) => sum + t.tokenCount, 0) + this.adapter.estimateTokens(this.context.systemPrompt || "");
|
|
2364
3247
|
this.context = await this.contextManager.prepare(this.context);
|
|
2365
|
-
|
|
3248
|
+
const systemPrompt = this.buildSystemPrompt(userMessageStr, this.iterations);
|
|
3249
|
+
let messages = this.buildMessages(systemPrompt, this.iterations);
|
|
2366
3250
|
if (this.stepCounter.isMaxReached()) {
|
|
2367
3251
|
this.logger.warn(`maxSteps (${this.config.maxSteps ?? 20}) reached \u2014 forcing final response`);
|
|
2368
3252
|
messages.push({
|
|
2369
3253
|
role: "system",
|
|
2370
3254
|
content: this.stepCounter.getForcedConclusionPrompt() + "\n\n" + FinalResponseFormatter.getRequiredStructure()
|
|
2371
3255
|
});
|
|
2372
|
-
this.emitTrace("planning", "max_steps_reached", {
|
|
3256
|
+
this.emitTrace("planning", "max_steps_reached", {
|
|
3257
|
+
maxSteps: this.config.maxSteps,
|
|
3258
|
+
currentSteps: this.stepCounter.count
|
|
3259
|
+
});
|
|
2373
3260
|
}
|
|
2374
3261
|
this.logger.debug(`Calling provider adapter (${this.adapter.name})...`);
|
|
2375
3262
|
this.emitTrace("thinking", "llm_call", {
|
|
@@ -2383,7 +3270,7 @@ var SessionManager = class {
|
|
|
2383
3270
|
model: this.config.model,
|
|
2384
3271
|
messages,
|
|
2385
3272
|
tools: this.stepCounter.isMaxReached() ? [] : this.toolRegistry.getAll(),
|
|
2386
|
-
maxTokens:
|
|
3273
|
+
maxTokens: maxCompletionTokens
|
|
2387
3274
|
});
|
|
2388
3275
|
} catch (err) {
|
|
2389
3276
|
const e = err;
|
|
@@ -2476,13 +3363,14 @@ var SessionManager = class {
|
|
|
2476
3363
|
this.context.turns.push(toolTurn);
|
|
2477
3364
|
if (this.config.onTurn) this.config.onTurn(toolTurn);
|
|
2478
3365
|
}
|
|
3366
|
+
if (this.goalInjector) this.goalInjector.incrementTurn();
|
|
2479
3367
|
continue;
|
|
2480
3368
|
}
|
|
2481
3369
|
if (response.finishReason === "stop" || response.finishReason === "max_tokens" || response.finishReason === "error") {
|
|
2482
3370
|
const finalTurn = {
|
|
2483
3371
|
role: "assistant",
|
|
2484
3372
|
content: response.content,
|
|
2485
|
-
tokenCount: response.usage.
|
|
3373
|
+
tokenCount: response.usage?.completionTokens ?? this.adapter.estimateTokens(response.content),
|
|
2486
3374
|
turnIndex: this.context.turns.length,
|
|
2487
3375
|
compressed: false
|
|
2488
3376
|
};
|
|
@@ -2512,18 +3400,32 @@ var SessionManager = class {
|
|
|
2512
3400
|
* @returns An `AsyncIterable<string>` of delta tokens from the final response
|
|
2513
3401
|
*
|
|
2514
3402
|
* @example
|
|
3403
|
+
* ```typescript
|
|
2515
3404
|
* for await (const token of session.stream('Tell me a story')) {
|
|
2516
3405
|
* process.stdout.write(token);
|
|
2517
3406
|
* }
|
|
3407
|
+
* ```
|
|
2518
3408
|
*/
|
|
2519
3409
|
async *stream(userMessage) {
|
|
2520
3410
|
if (this.mcpReady) await this.mcpReady;
|
|
3411
|
+
await this.ensureScratchpadLoaded();
|
|
2521
3412
|
const userMessageStr = Array.isArray(userMessage) ? "[Multimodal Content]" : userMessage;
|
|
2522
3413
|
this.logger.info(`Starting streaming session run`, {
|
|
2523
3414
|
model: this.config.model,
|
|
2524
3415
|
message: userMessageStr
|
|
2525
3416
|
});
|
|
2526
|
-
|
|
3417
|
+
if (this.config.enableGoalPlanning && !this.goalInjector) {
|
|
3418
|
+
this.goalInjector = new GoalInjector({
|
|
3419
|
+
id: "auto",
|
|
3420
|
+
statement: typeof userMessage === "string" ? userMessage : "[multimodal]",
|
|
3421
|
+
decomposition: [],
|
|
3422
|
+
successCriteria: ["The user request is fully answered"],
|
|
3423
|
+
injectionFrequency: this.config.goalInjectionFrequency ?? "always",
|
|
3424
|
+
injectionPosition: this.config.goalInjectionPosition ?? "system_prompt"
|
|
3425
|
+
});
|
|
3426
|
+
this.context.metadata["goal"] = this.goalInjector.getGoal();
|
|
3427
|
+
await this._runMiniPlanningStep(userMessageStr);
|
|
3428
|
+
}
|
|
2527
3429
|
this.context.turns.push({
|
|
2528
3430
|
role: "user",
|
|
2529
3431
|
content: userMessage,
|
|
@@ -2534,11 +3436,14 @@ var SessionManager = class {
|
|
|
2534
3436
|
const maxIts = this.config.maxIterations || 10;
|
|
2535
3437
|
this.iterations = 0;
|
|
2536
3438
|
this.stepCounter = new StepCounter(this.config.maxSteps ?? 20);
|
|
3439
|
+
const maxCompletionTokens = this.config.maxCompletionTokens ?? 2e3;
|
|
2537
3440
|
while (this.iterations < maxIts) {
|
|
2538
3441
|
this.iterations++;
|
|
2539
3442
|
this.logger.debug(`[stream] ReAct Iteration ${this.iterations}/${maxIts}`);
|
|
3443
|
+
this.context.tokenCount = this.context.turns.reduce((sum, t) => sum + t.tokenCount, 0) + this.adapter.estimateTokens(this.context.systemPrompt || "");
|
|
2540
3444
|
this.context = await this.contextManager.prepare(this.context);
|
|
2541
|
-
const
|
|
3445
|
+
const systemPrompt = this.buildSystemPrompt(userMessageStr, this.iterations);
|
|
3446
|
+
const messages = this.buildMessages(systemPrompt, this.iterations);
|
|
2542
3447
|
if (this.stepCounter.isMaxReached()) {
|
|
2543
3448
|
messages.push({
|
|
2544
3449
|
role: "system",
|
|
@@ -2551,7 +3456,7 @@ var SessionManager = class {
|
|
|
2551
3456
|
model: this.config.model,
|
|
2552
3457
|
messages,
|
|
2553
3458
|
tools: this.stepCounter.isMaxReached() ? [] : this.toolRegistry.getAll(),
|
|
2554
|
-
maxTokens:
|
|
3459
|
+
maxTokens: maxCompletionTokens
|
|
2555
3460
|
});
|
|
2556
3461
|
} catch (err) {
|
|
2557
3462
|
this.logger.fatal(`Provider call failed: ${err.message}`);
|
|
@@ -2594,17 +3499,20 @@ var SessionManager = class {
|
|
|
2594
3499
|
this.context.turns.push(toolTurn);
|
|
2595
3500
|
if (this.config.onTurn) this.config.onTurn(toolTurn);
|
|
2596
3501
|
}
|
|
3502
|
+
if (this.goalInjector) this.goalInjector.incrementTurn();
|
|
2597
3503
|
continue;
|
|
2598
3504
|
}
|
|
3505
|
+
this.context.tokenCount = this.context.turns.reduce((sum, t) => sum + t.tokenCount, 0) + this.adapter.estimateTokens(this.context.systemPrompt || "");
|
|
2599
3506
|
this.context = await this.contextManager.prepare(this.context);
|
|
2600
|
-
const
|
|
3507
|
+
const finalSystemPrompt = this.buildSystemPrompt(userMessageStr, this.iterations);
|
|
3508
|
+
const finalMessages = this.buildMessages(finalSystemPrompt, this.iterations);
|
|
2601
3509
|
let accumulated = "";
|
|
2602
3510
|
let finalFinishReason = void 0;
|
|
2603
3511
|
let finalTokenCount = 0;
|
|
2604
3512
|
for await (const chunk of this.adapter.stream({
|
|
2605
3513
|
model: this.config.model,
|
|
2606
3514
|
messages: finalMessages,
|
|
2607
|
-
maxTokens:
|
|
3515
|
+
maxTokens: maxCompletionTokens,
|
|
2608
3516
|
stream: true
|
|
2609
3517
|
})) {
|
|
2610
3518
|
if (chunk.delta) {
|
|
@@ -2645,7 +3553,8 @@ var SessionManager = class {
|
|
|
2645
3553
|
}
|
|
2646
3554
|
/**
|
|
2647
3555
|
* Resets the session: clears conversation history, resets iteration counters,
|
|
2648
|
-
*
|
|
3556
|
+
* tool execution budget tallies, and goal/plan state.
|
|
3557
|
+
* The adapter, config, compression strategies, and tools are retained.
|
|
2649
3558
|
*/
|
|
2650
3559
|
reset() {
|
|
2651
3560
|
this.context = {
|
|
@@ -2658,24 +3567,30 @@ var SessionManager = class {
|
|
|
2658
3567
|
};
|
|
2659
3568
|
this.iterations = 0;
|
|
2660
3569
|
this.totalToolCallCount = 0;
|
|
3570
|
+
this.totalTokens = 0;
|
|
2661
3571
|
this.perToolCallCount.clear();
|
|
2662
3572
|
this.stepCounter = new StepCounter(this.config.maxSteps ?? 20);
|
|
2663
3573
|
this.goalInjector = null;
|
|
3574
|
+
this.continuationPlanner = null;
|
|
3575
|
+
this.scratchpadLoaded = false;
|
|
3576
|
+
this.pendingScratchpadClear = !!this.config.scratchpadAdapter;
|
|
2664
3577
|
this.logger.debug("Session reset");
|
|
2665
3578
|
}
|
|
2666
3579
|
/**
|
|
2667
3580
|
* Closes the session and disconnects all MCP servers.
|
|
2668
3581
|
*
|
|
2669
|
-
* Call this when you are done with the session
|
|
2670
|
-
*
|
|
3582
|
+
* Call this when you are done with the session to ensure child processes are
|
|
3583
|
+
* terminated and HTTP connections are released.
|
|
2671
3584
|
*
|
|
2672
3585
|
* @example
|
|
3586
|
+
* ```typescript
|
|
2673
3587
|
* const session = new SessionManager({ ..., mcpServers: [...] });
|
|
2674
3588
|
* try {
|
|
2675
3589
|
* await session.run('Hello');
|
|
2676
3590
|
* } finally {
|
|
2677
3591
|
* await session.close();
|
|
2678
3592
|
* }
|
|
3593
|
+
* ```
|
|
2679
3594
|
*/
|
|
2680
3595
|
async close() {
|
|
2681
3596
|
if (this.mcpRegistry) {
|
|
@@ -2686,37 +3601,6 @@ var SessionManager = class {
|
|
|
2686
3601
|
}
|
|
2687
3602
|
};
|
|
2688
3603
|
|
|
2689
|
-
|
|
2690
|
-
var ContinuationPlanner = class {
|
|
2691
|
-
constructor(plan) {
|
|
2692
|
-
this.plan = plan;
|
|
2693
|
-
}
|
|
2694
|
-
getPlanStatusString() {
|
|
2695
|
-
let result = `[CONTINUATION PLAN \u2014 Step ${this.plan.currentStepIndex + 1}/${this.plan.steps.length}]
|
|
2696
|
-
`;
|
|
2697
|
-
for (const step of this.plan.steps) {
|
|
2698
|
-
const getIcon = (status) => {
|
|
2699
|
-
switch (status) {
|
|
2700
|
-
case "done":
|
|
2701
|
-
return "\u2705";
|
|
2702
|
-
case "running":
|
|
2703
|
-
return "\u25B6";
|
|
2704
|
-
case "failed":
|
|
2705
|
-
return "\u274C";
|
|
2706
|
-
case "skipped":
|
|
2707
|
-
return "\u23ED";
|
|
2708
|
-
default:
|
|
2709
|
-
return "\u23F3";
|
|
2710
|
-
}
|
|
2711
|
-
};
|
|
2712
|
-
const statusText = step.status === "pending" && step.dependsOn.length > 0 ? `Waiting on Step ${step.dependsOn.join(", ")}` : step.status.charAt(0).toUpperCase() + step.status.slice(1);
|
|
2713
|
-
result += `${getIcon(step.status)} Step ${step.stepId} (${step.toolName}): ${statusText}
|
|
2714
|
-
`;
|
|
2715
|
-
}
|
|
2716
|
-
return result;
|
|
2717
|
-
}
|
|
2718
|
-
};
|
|
2719
|
-
|
|
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 };
|
|
3604
|
+
export { ContextManager, ContinuationPlanner, DefaultLogger, FinalResponseFormatter, GoalInjector, HistoryCompressionStrategy, InMemoryRAGAdapter, InMemoryScratchpadAdapter, InMemoryStorageAdapter, LemuraAdapterError, LemuraContextOverflowError, LemuraError, LemuraMCPConnectionError, LemuraMCPError, LemuraMCPTimeoutError, LemuraMaxIterationsError, LemuraSkillInjectionError, LemuraToolNotFoundError, LemuraToolTimeoutError, LemuraToolValidationError, LogLevel, MCPClient, MCPClientRegistry, MediaBridge, OpenAICompatibleAdapter, SandwichCompressionStrategy, ScratchpadStrategy, SessionManager, ShortTermMemoryRegistry, SkillInjector, StepCounter, SummaryInjectionStrategy, ToolRegistry, ToolResponseProcessor, evaluateToolFirewall, validateJsonSchema };
|
|
2721
3605
|
//# sourceMappingURL=index.mjs.map
|
|
2722
3606
|
//# sourceMappingURL=index.mjs.map
|