llmist 17.5.1 → 18.0.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.cjs CHANGED
@@ -1414,7 +1414,7 @@ Produces: { "items": ["first", "second"] }`);
1414
1414
  * @param mediaIds - Optional IDs for the media outputs
1415
1415
  * @param storedMedia - Optional stored media info including file paths
1416
1416
  */
1417
- addGadgetCallResult(gadget, parameters, result, invocationId, media, mediaIds, storedMedia) {
1417
+ addGadgetCallResult(gadget, parameters, result, invocationId, media, mediaIds, storedMedia, metadata) {
1418
1418
  const paramStr = this.formatBlockParameters(parameters, "");
1419
1419
  this.messages.push({
1420
1420
  role: "assistant",
@@ -1438,11 +1438,12 @@ ${idRefs}`;
1438
1438
  parts.push(audioFromBase64(item.data, item.mimeType));
1439
1439
  }
1440
1440
  }
1441
- this.messages.push({ role: "user", content: parts });
1441
+ this.messages.push({ role: "user", content: parts, metadata });
1442
1442
  } else {
1443
1443
  this.messages.push({
1444
1444
  role: "user",
1445
- content: `Result (${invocationId}): ${result}`
1445
+ content: `Result (${invocationId}): ${result}`,
1446
+ metadata
1446
1447
  });
1447
1448
  }
1448
1449
  return this;
@@ -2674,11 +2675,19 @@ var init_sliding_window = __esm({
2674
2675
  }
2675
2676
  const turnsToKeep = turns.slice(-preserveCount);
2676
2677
  const turnsRemoved = turns.length - preserveCount;
2678
+ const keptMessageRefs = new Set(turnsToKeep.flatMap((turn) => turn.messages));
2679
+ const stickyToPreserve = messages.filter(
2680
+ (msg) => msg.metadata?.sticky === true && !keptMessageRefs.has(msg)
2681
+ );
2677
2682
  const truncationMarker = {
2678
2683
  role: "user",
2679
2684
  content: TRUNCATION_MARKER_TEMPLATE.replace("{count}", turnsRemoved.toString())
2680
2685
  };
2681
- const compactedMessages = [truncationMarker, ...flattenTurns(turnsToKeep)];
2686
+ const compactedMessages = [
2687
+ truncationMarker,
2688
+ ...stickyToPreserve,
2689
+ ...flattenTurns(turnsToKeep)
2690
+ ];
2682
2691
  const tokensAfter = Math.ceil(
2683
2692
  compactedMessages.reduce((sum, msg) => sum + (msg.content?.length ?? 0), 0) / 4
2684
2693
  );
@@ -2722,7 +2731,14 @@ var init_summarization = __esm({
2722
2731
  }
2723
2732
  const turnsToSummarize = turns.slice(0, -preserveCount);
2724
2733
  const turnsToKeep = turns.slice(-preserveCount);
2725
- const conversationToSummarize = this.formatTurnsForSummary(flattenTurns(turnsToSummarize));
2734
+ const keptMessageRefs = new Set(turnsToKeep.flatMap((turn) => turn.messages));
2735
+ const stickyToPreserve = messages.filter(
2736
+ (msg) => msg.metadata?.sticky === true && !keptMessageRefs.has(msg)
2737
+ );
2738
+ const turnsToSummarizeMessages = flattenTurns(turnsToSummarize).filter(
2739
+ (msg) => msg.metadata?.sticky !== true
2740
+ );
2741
+ const conversationToSummarize = this.formatTurnsForSummary(turnsToSummarizeMessages);
2726
2742
  const summary = await this.generateSummary(conversationToSummarize, config, context);
2727
2743
  const summaryMessage = {
2728
2744
  role: "user",
@@ -2730,7 +2746,11 @@ var init_summarization = __esm({
2730
2746
  ${summary}
2731
2747
  [End of summary - conversation continues below]`
2732
2748
  };
2733
- const compactedMessages = [summaryMessage, ...flattenTurns(turnsToKeep)];
2749
+ const compactedMessages = [
2750
+ summaryMessage,
2751
+ ...stickyToPreserve,
2752
+ ...flattenTurns(turnsToKeep)
2753
+ ];
2734
2754
  const tokensAfter = Math.ceil(
2735
2755
  compactedMessages.reduce((sum, msg) => sum + (msg.content?.length ?? 0), 0) / 4
2736
2756
  );
@@ -3051,7 +3071,7 @@ var init_conversation_manager = __esm({
3051
3071
  addAssistantMessage(content) {
3052
3072
  this.historyBuilder.addAssistant(content);
3053
3073
  }
3054
- addGadgetCallResult(gadgetName, parameters, result, invocationId, media, mediaIds, storedMedia) {
3074
+ addGadgetCallResult(gadgetName, parameters, result, invocationId, media, mediaIds, storedMedia, metadata) {
3055
3075
  this.historyBuilder.addGadgetCallResult(
3056
3076
  gadgetName,
3057
3077
  parameters,
@@ -3059,7 +3079,8 @@ var init_conversation_manager = __esm({
3059
3079
  invocationId,
3060
3080
  media,
3061
3081
  mediaIds,
3062
- storedMedia
3082
+ storedMedia,
3083
+ metadata
3063
3084
  );
3064
3085
  }
3065
3086
  getMessages() {
@@ -3191,6 +3212,7 @@ var init_conversation_updater = __esm({
3191
3212
  for (const output of gadgetResults) {
3192
3213
  if (output.type === "gadget_result") {
3193
3214
  const gadgetResult = output.result;
3215
+ const metadata = gadgetResult.stickyResult === true && gadgetResult.error === void 0 ? { sticky: true } : void 0;
3194
3216
  this.conversation.addGadgetCallResult(
3195
3217
  gadgetResult.gadgetName,
3196
3218
  gadgetResult.parameters,
@@ -3198,7 +3220,8 @@ var init_conversation_updater = __esm({
3198
3220
  gadgetResult.invocationId,
3199
3221
  gadgetResult.media,
3200
3222
  gadgetResult.mediaIds,
3201
- gadgetResult.storedMedia
3223
+ gadgetResult.storedMedia,
3224
+ metadata
3202
3225
  );
3203
3226
  }
3204
3227
  }
@@ -4460,6 +4483,43 @@ var init_gadget = __esm({
4460
4483
  * This is a safety floor: external config cannot weaken it.
4461
4484
  */
4462
4485
  exclusive;
4486
+ /**
4487
+ * If true, results produced by this gadget are marked sticky on the
4488
+ * conversation (`message.metadata.sticky === true`). Compaction strategies
4489
+ * preserve sticky messages past the truncation point, so the agent retains
4490
+ * the gadget's output for the rest of the conversation rather than having
4491
+ * it dropped on the next compaction pass.
4492
+ *
4493
+ * Use for gadgets whose output is *reference material* the agent will keep
4494
+ * consulting — `LoadSkill` is the canonical example: a multi-KB skill body
4495
+ * the agent needs to remember across iterations. Don't use for routine
4496
+ * gadget outputs (file reads, computation results) — those should churn
4497
+ * normally with the conversation.
4498
+ *
4499
+ * Has no effect on agents that don't enable compaction.
4500
+ */
4501
+ stickyResult;
4502
+ /**
4503
+ * Hints to the consuming agent loop that when this gadget appears in an
4504
+ * LLM iteration's tool batch, no other gadget in the same batch should
4505
+ * execute. Sibling tool calls in the same iteration are expected to be
4506
+ * skipped (not executed) with a synthetic result; the next LLM iteration
4507
+ * gets only this gadget's output back, and must re-plan from there.
4508
+ *
4509
+ * llmist exposes this as declarative metadata only — enforcement is the
4510
+ * consuming agent loop's responsibility (the loop already owns the
4511
+ * stream-event consumption and the `beforeGadgetExecution` controller, so
4512
+ * it can buffer per-iteration calls, decide barrier-status at
4513
+ * `llm_response_end`, and skip non-barrier siblings via the standard
4514
+ * skip-with-synthetic-result mechanism). See `LoadSkill` for the canonical
4515
+ * use case: the agent loop should freeze sibling tool execution so the
4516
+ * LLM sees only the loaded skill body before issuing dependent work.
4517
+ *
4518
+ * Orthogonal to `stickyResult` (which affects compaction) and `exclusive`
4519
+ * (which queues the marked gadget alone, AFTER others — opposite of this
4520
+ * flag's "freeze the others" semantic).
4521
+ */
4522
+ iterationBarrier;
4463
4523
  /**
4464
4524
  * Throws an AbortException if the execution has been aborted.
4465
4525
  *
@@ -4656,6 +4716,8 @@ function createGadget(config) {
4656
4716
  timeoutMs = config.timeoutMs;
4657
4717
  examples = config.examples;
4658
4718
  maxConcurrent = config.maxConcurrent;
4719
+ stickyResult = config.stickyResult;
4720
+ iterationBarrier = config.iterationBarrier;
4659
4721
  execute(params, ctx) {
4660
4722
  return config.execute(params, ctx);
4661
4723
  }
@@ -5478,11 +5540,20 @@ var init_activation = __esm({
5478
5540
  });
5479
5541
 
5480
5542
  // src/skills/load-skill-gadget.ts
5543
+ function composeSkillSections(sections) {
5544
+ return sections.map(({ name, body }) => `==== ${name} ====
5545
+ ${body}`).join("\n\n");
5546
+ }
5481
5547
  function createLoadSkillGadget(registry) {
5482
5548
  const summaries = registry.getMetadataSummaries();
5483
5549
  const skillNames = registry.getModelInvocable().map((s) => s.name);
5484
5550
  const description = [
5485
- "Load a skill's specialized instructions into context for a task.",
5551
+ "Load one or more skill bodies into context. Pass an array even for a",
5552
+ 'single skill: `{skills: ["some-skill"]}`. **This gadget is an iteration',
5553
+ "barrier \u2014 no other gadgets in the same tool batch will execute, so load",
5554
+ "every skill you know you'll need in one shot.** The loaded bodies are",
5555
+ "sticky and survive context compaction.",
5556
+ "",
5486
5557
  "Available skills:",
5487
5558
  summaries
5488
5559
  ].join("\n");
@@ -5490,19 +5561,33 @@ function createLoadSkillGadget(registry) {
5490
5561
  name: LOAD_SKILL_GADGET_NAME,
5491
5562
  description,
5492
5563
  schema: import_zod2.z.object({
5493
- skill: import_zod2.z.enum(skillNames).describe("Name of the skill to load"),
5494
- arguments: import_zod2.z.string().optional().describe("Arguments for the skill (e.g., a filename, issue number, or search query)")
5564
+ skills: import_zod2.z.array(import_zod2.z.enum(skillNames)).min(1).describe(
5565
+ "One or more skill names to load in this single call. Prefer loading everything you know you'll need at once \u2014 this gadget is an iteration barrier, so sibling tool calls in the same batch will not execute."
5566
+ ),
5567
+ arguments: import_zod2.z.string().optional().describe(
5568
+ "Optional argument string substituted into each skill's $ARGUMENTS placeholders. Applies to every skill in the batch. To pass different arguments to different skills, issue separate LoadSkill calls."
5569
+ )
5495
5570
  }),
5496
- execute: async ({ skill: skillName, arguments: args }) => {
5497
- const skill = registry.get(skillName);
5498
- if (!skill) {
5499
- return `Unknown skill: "${skillName}". Available skills: ${skillNames.join(", ")}`;
5500
- }
5501
- const activation = await skill.activate({
5502
- arguments: args,
5503
- cwd: process.cwd()
5504
- });
5505
- return activation.resolvedInstructions;
5571
+ stickyResult: true,
5572
+ iterationBarrier: true,
5573
+ execute: async ({ skills: skillNamesArg, arguments: args }) => {
5574
+ const sections = [];
5575
+ for (const skillName of skillNamesArg) {
5576
+ const skill = registry.get(skillName);
5577
+ if (!skill) {
5578
+ sections.push({
5579
+ name: skillName,
5580
+ body: `Unknown skill: "${skillName}". Available skills: ${skillNames.join(", ")}`
5581
+ });
5582
+ continue;
5583
+ }
5584
+ const activation = await skill.activate({
5585
+ arguments: args,
5586
+ cwd: process.cwd()
5587
+ });
5588
+ sections.push({ name: skillName, body: activation.resolvedInstructions });
5589
+ }
5590
+ return composeSkillSections(sections);
5506
5591
  }
5507
5592
  });
5508
5593
  }
@@ -14875,7 +14960,13 @@ var init_executor = __esm({
14875
14960
  cost: totalCost,
14876
14961
  media,
14877
14962
  mediaIds,
14878
- storedMedia
14963
+ storedMedia,
14964
+ // Copy the gadget's sticky flag into the result envelope so the
14965
+ // conversation-updater can mark the persisted message sticky for
14966
+ // the compaction layer. Only set on success — errors and other
14967
+ // unsuccessful exits skip this so failed loads don't pin themselves
14968
+ // in context.
14969
+ stickyResult: gadget.stickyResult
14879
14970
  };
14880
14971
  } catch (error) {
14881
14972
  const isTaskCompletionSignal = error instanceof Error && error.name === "TaskCompletionSignal";