@cognipeer/agent-sdk 0.10.1 → 0.10.3

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
@@ -4601,18 +4601,60 @@ function createToolsNode(initialTools, opts) {
4601
4601
  toolStateRef.__currentToolCallId = toolCallId;
4602
4602
  delete toolStateRef.__awaitingApproval;
4603
4603
  }
4604
- let hookAskedForApproval = false;
4605
- let hookApprovalPrompt;
4606
- let hookResult;
4607
- if (host?.has("preToolUse")) {
4604
+ const stableJson = (value) => {
4605
+ try {
4606
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
4607
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
4608
+ const keys = Object.keys(value).sort();
4609
+ return `{${keys.map((k) => JSON.stringify(k) + ":" + stableJson(value[k])).join(",")}}`;
4610
+ } catch {
4611
+ return String(value);
4612
+ }
4613
+ };
4614
+ const rejectByPolicy = (gateReason, gatedArgs) => {
4615
+ const reason = gateReason || `Tool call blocked by policy: ${toolName}`;
4616
+ push({ role: "tool", content: reason, tool_call_id: toolCallId, name: tc.name });
4617
+ toolHistory.push({
4618
+ executionId: nanoid.nanoid(),
4619
+ toolName,
4620
+ args: gatedArgs,
4621
+ output: reason,
4622
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4623
+ tool_call_id: toolCallId,
4624
+ status: "rejected"
4625
+ });
4626
+ onEvent?.({ type: "tool_call", phase: "skipped", name: toolName, id: tc.id, args: gatedArgs, error: { message: reason } });
4627
+ recordTraceEvent(traceSession, {
4628
+ type: "tool_call",
4629
+ label: `Tool Denied - ${toolName}`,
4630
+ actor: { scope: "tool", name: toolName, role: "tool" },
4631
+ status: "skipped",
4632
+ toolDetails,
4633
+ toolExecutionId: toolCallId,
4634
+ sections: buildToolTraceSections({
4635
+ args: gatedArgs,
4636
+ executionId: toolCallId,
4637
+ output: reason,
4638
+ status: "skipped",
4639
+ summary: reason,
4640
+ toolDetails,
4641
+ toolName
4642
+ })
4643
+ });
4644
+ toolCount += 1;
4645
+ return { status: "error" };
4646
+ };
4647
+ const applyPreToolUseGate = async (candidate) => {
4648
+ if (!host?.has("preToolUse")) return { ok: true, args: candidate, ask: false };
4608
4649
  const gate = await host.runGate("preToolUse", {
4609
4650
  toolName,
4610
4651
  toolCallId,
4611
- args,
4652
+ args: candidate,
4612
4653
  tool,
4613
4654
  executionCount: countSuccessfulToolExecutions(toolHistory, toolName)
4614
4655
  });
4615
- if (gate.input.args !== args) {
4656
+ let next = candidate;
4657
+ if (gate.input.args !== candidate) {
4616
4658
  const revalidated = validateToolArgs(tool, gate.input.args);
4617
4659
  if (!revalidated.ok && resolved.toolResponses.schemaValidation === "strict") {
4618
4660
  const message = `Tool argument validation failed after a plugin rewrite for ${toolName}: ${revalidated.message}`;
@@ -4620,48 +4662,31 @@ function createToolsNode(initialTools, opts) {
4620
4662
  markToolFailure(message, toolName, toolCallId);
4621
4663
  onEvent?.({ type: "tool_call", phase: "error", name: toolName, id: tc.id, args: gate.input.args, error: { message } });
4622
4664
  toolCount += 1;
4623
- return { status: "error" };
4665
+ return { ok: false, outcome: { status: "error" } };
4624
4666
  }
4625
- args = revalidated.ok ? revalidated.value : gate.input.args;
4667
+ next = revalidated.ok ? revalidated.value : gate.input.args;
4626
4668
  }
4627
4669
  if (gate.decision === "deny") {
4628
- const reason = gate.reason || `Tool call blocked by policy: ${toolName}`;
4629
- push({ role: "tool", content: reason, tool_call_id: toolCallId, name: tc.name });
4630
- toolHistory.push({
4631
- executionId: nanoid.nanoid(),
4632
- toolName,
4633
- args,
4634
- output: reason,
4635
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4636
- tool_call_id: toolCallId,
4637
- status: "rejected"
4638
- });
4639
- onEvent?.({ type: "tool_call", phase: "skipped", name: toolName, id: tc.id, args, error: { message: reason } });
4640
- recordTraceEvent(traceSession, {
4641
- type: "tool_call",
4642
- label: `Tool Denied - ${toolName}`,
4643
- actor: { scope: "tool", name: toolName, role: "tool" },
4644
- status: "skipped",
4645
- toolDetails,
4646
- toolExecutionId: toolCallId,
4647
- sections: buildToolTraceSections({
4648
- args,
4649
- executionId: toolCallId,
4650
- output: reason,
4651
- status: "skipped",
4652
- summary: reason,
4653
- toolDetails,
4654
- toolName
4655
- })
4656
- });
4657
- toolCount += 1;
4658
- return { status: "error" };
4659
- }
4660
- if (gate.decision === "ask") {
4661
- hookAskedForApproval = true;
4662
- hookApprovalPrompt = gate.approvalPrompt;
4670
+ return { ok: false, outcome: rejectByPolicy(gate.reason, next) };
4663
4671
  }
4664
- if (gate.shortCircuit !== void 0) hookResult = gate.shortCircuit;
4672
+ return {
4673
+ ok: true,
4674
+ args: next,
4675
+ ask: gate.decision === "ask",
4676
+ approvalPrompt: gate.approvalPrompt,
4677
+ result: gate.shortCircuit
4678
+ };
4679
+ };
4680
+ let hookAskedForApproval = false;
4681
+ let hookApprovalPrompt;
4682
+ let hookResult;
4683
+ {
4684
+ const gated = await applyPreToolUseGate(args);
4685
+ if (!gated.ok) return gated.outcome;
4686
+ args = gated.args;
4687
+ hookAskedForApproval = gated.ask;
4688
+ hookApprovalPrompt = gated.approvalPrompt;
4689
+ if (gated.result !== void 0) hookResult = gated.result;
4665
4690
  }
4666
4691
  const maxExecutionsPerRun = normalizeMaxExecutionsPerRun(tool.maxExecutionsPerRun);
4667
4692
  if (maxExecutionsPerRun !== null) {
@@ -4746,7 +4771,34 @@ function createToolsNode(initialTools, opts) {
4746
4771
  return { status: "rejected", approval: approvalEntry };
4747
4772
  }
4748
4773
  if (approvalEntry.status === "approved" && approvalEntry.approvedArgs !== void 0) {
4749
- args = approvalEntry.approvedArgs;
4774
+ const approvedArgs = approvalEntry.approvedArgs;
4775
+ if (stableJson(approvedArgs) !== stableJson(args)) {
4776
+ const settleRefused = (resolution) => {
4777
+ approvalEntry.metadata = { ...approvalEntry.metadata || {}, resolution };
4778
+ approvalEntry.status = "executed";
4779
+ approvalEntry.resolvedAt = (/* @__PURE__ */ new Date()).toISOString();
4780
+ pendingByCallId.set(toolCallId, approvalEntry);
4781
+ };
4782
+ const revalidated = validateToolArgs(tool, approvedArgs);
4783
+ if (!revalidated.ok && resolved.toolResponses.schemaValidation === "strict") {
4784
+ const message = `Tool argument validation failed for reviewer-edited arguments of ${toolName}: ${revalidated.message}`;
4785
+ push({ role: "tool", content: message, tool_call_id: toolCallId, name: tc.name });
4786
+ markToolFailure(message, toolName, toolCallId);
4787
+ settleRefused("invalid_args");
4788
+ onEvent?.({ type: "tool_call", phase: "error", name: toolName, id: tc.id, args: approvedArgs, error: { message } });
4789
+ toolCount += 1;
4790
+ return { status: "error", approval: approvalEntry };
4791
+ }
4792
+ const gated = await applyPreToolUseGate(revalidated.ok ? revalidated.value : approvedArgs);
4793
+ if (!gated.ok) {
4794
+ settleRefused("policy_denied");
4795
+ return { ...gated.outcome, approval: approvalEntry };
4796
+ }
4797
+ args = gated.args;
4798
+ if (gated.result !== void 0 && hookResult === void 0) hookResult = gated.result;
4799
+ } else {
4800
+ args = approvedArgs;
4801
+ }
4750
4802
  onEvent?.({ type: "tool_approval", status: "approved", id: approvalEntry.id, toolName: approvalEntry.toolName, toolCallId: approvalEntry.toolCallId, decidedBy: approvalEntry.decidedBy, comment: approvalEntry.comment });
4751
4803
  }
4752
4804
  }
@@ -4756,16 +4808,6 @@ function createToolsNode(initialTools, opts) {
4756
4808
  const cacheCfg = tool.cache;
4757
4809
  const cacheEnabled = cacheCfg === true || cacheCfg && typeof cacheCfg === "object";
4758
4810
  const cacheKeyFn = cacheEnabled && typeof cacheCfg === "object" ? cacheCfg.keyFn : void 0;
4759
- const stableJson = (value) => {
4760
- try {
4761
- if (value === null || typeof value !== "object") return JSON.stringify(value);
4762
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
4763
- const keys = Object.keys(value).sort();
4764
- return `{${keys.map((k) => JSON.stringify(k) + ":" + stableJson(value[k])).join(",")}}`;
4765
- } catch {
4766
- return String(value);
4767
- }
4768
- };
4769
4811
  const cacheKey = cacheEnabled ? `${toolName}|${cacheKeyFn ? cacheKeyFn(args) : stableJson(args)}` : void 0;
4770
4812
  const toolCache = state.toolCache = state.toolCache || {};
4771
4813
  let cachedHit = void 0;
@@ -4813,6 +4855,22 @@ function createToolsNode(initialTools, opts) {
4813
4855
  try {
4814
4856
  if (cachedHit !== void 0) {
4815
4857
  const executionId2 = nanoid.nanoid();
4858
+ if (host?.has("postToolUse")) {
4859
+ const shortGate = await host.runGate("postToolUse", {
4860
+ toolName,
4861
+ toolCallId,
4862
+ args,
4863
+ output: cachedHit,
4864
+ durationMs: 0,
4865
+ executionId: executionId2,
4866
+ source: shortCircuitSource === "plugin" ? "hook" : "cache"
4867
+ });
4868
+ if (shortGate.decision === "deny") {
4869
+ cachedHit = shortGate.reason || `Tool output withheld by policy: ${toolName}`;
4870
+ } else if (shortGate.input.output !== cachedHit) {
4871
+ cachedHit = shortGate.input.output;
4872
+ }
4873
+ }
4816
4874
  const { output: compressedCachedHit, stats: cachedContextPilotStats } = maybeCompressToolOutput(toolName, cachedHit, executionId2);
4817
4875
  const responsePolicy2 = applyToolResponseHardCap(toolName, compressedCachedHit, executionId2, resolved);
4818
4876
  toolHistory.push({
@@ -4979,7 +5037,8 @@ function createToolsNode(initialTools, opts) {
4979
5037
  args,
4980
5038
  output,
4981
5039
  durationMs,
4982
- executionId
5040
+ executionId,
5041
+ source: "tool"
4983
5042
  });
4984
5043
  if (postGate.decision === "deny") {
4985
5044
  output = postGate.reason || `Tool output withheld by policy: ${toolName}`;
@@ -5074,7 +5133,8 @@ function createToolsNode(initialTools, opts) {
5074
5133
  output: message,
5075
5134
  error: error instanceof Error ? error : new Error(message),
5076
5135
  durationMs,
5077
- executionId
5136
+ executionId,
5137
+ source: "tool"
5078
5138
  });
5079
5139
  if (errorGate.decision === "deny") {
5080
5140
  message = errorGate.reason || `Tool error withheld by policy: ${toolName}`;
@@ -5750,7 +5810,14 @@ var DISALLOWED_CTX_KEYS = /* @__PURE__ */ new Set([
5750
5810
  // Session bookkeeping: a resumed run must open its OWN session, so these
5751
5811
  // must not travel in a snapshot.
5752
5812
  "__pluginRunStarted",
5753
- "__pluginRunEnded"
5813
+ "__pluginRunEnded",
5814
+ // Set by restoreSnapshot on the way OUT, consumed by the next invoke. Never
5815
+ // persisted: a snapshot of a restored run would otherwise mark every later
5816
+ // restore as "resumed" forever.
5817
+ "__restoredFromSnapshot",
5818
+ // A per-turn verdict. It describes the run that produced the snapshot, not
5819
+ // the run that will resume from it.
5820
+ "__guardrailBlocked"
5754
5821
  ]);
5755
5822
  var clone = (value) => {
5756
5823
  if (typeof globalThis.structuredClone === "function") {
@@ -6089,6 +6156,9 @@ function isSyntheticSummaryMessage(message) {
6089
6156
  }
6090
6157
  return false;
6091
6158
  }
6159
+ function isCompactedToolContent(content) {
6160
+ return typeof content === "string" && (content === "SUMMARIZED" || content.startsWith("SUMMARIZED_TOOL_RESPONSE") || content.startsWith("ARCHIVED_TOOL_RESPONSE") || content.startsWith("STRUCTURED_TOOL_RESPONSE") || content.startsWith("DROPPED_TOOL_RESPONSE"));
6161
+ }
6092
6162
 
6093
6163
  // src/structuredOutput/manager.ts
6094
6164
  var StructuredOutputManager = class {
@@ -6374,6 +6444,7 @@ var SLOT_NAMES = [
6374
6444
  "contextBuilder"
6375
6445
  ];
6376
6446
  var DECISION_RANK = { allow: 0, ask: 1, deny: 2 };
6447
+ var isHookDecision = (value) => typeof value === "string" && Object.prototype.hasOwnProperty.call(DECISION_RANK, value);
6377
6448
  function escalate(current, next) {
6378
6449
  return DECISION_RANK[next] > DECISION_RANK[current] ? next : current;
6379
6450
  }
@@ -6383,6 +6454,15 @@ var HookTimeoutError = class extends Error {
6383
6454
  this.name = "HookTimeoutError";
6384
6455
  }
6385
6456
  };
6457
+ var InvalidHookDecisionError = class extends Error {
6458
+ constructor(plugin, hook, value) {
6459
+ const shown = typeof value === "string" ? JSON.stringify(value) : String(value);
6460
+ super(
6461
+ `plugin "${plugin}" hook "${hook}" returned an unknown decision ${shown}; expected "allow", "ask" or "deny"`
6462
+ );
6463
+ this.name = "InvalidHookDecisionError";
6464
+ }
6465
+ };
6386
6466
  function withTimeout(value, ms, label) {
6387
6467
  if (!ms || !Number.isFinite(ms) || ms <= 0) return Promise.resolve(value);
6388
6468
  return new Promise((resolve, reject) => {
@@ -6672,10 +6752,18 @@ function createPluginHost(plugins = [], options = {}) {
6672
6752
  `plugin "${entry.plugin}" hook "${hook}"`
6673
6753
  );
6674
6754
  output = raw ?? void 0;
6755
+ if (output && typeof output === "object" && output.decision !== void 0 && !isHookDecision(output.decision)) {
6756
+ throw new InvalidHookDecisionError(entry.plugin, hook, output.decision);
6757
+ }
6675
6758
  } catch (err) {
6676
6759
  const message = err instanceof Error ? err.message : String(err);
6677
6760
  const timedOut = err instanceof HookTimeoutError;
6678
- logger.warn(`plugin "${entry.plugin}" hook "${hook}" failed:`, message);
6761
+ const badDecision = err instanceof InvalidHookDecisionError;
6762
+ if (badDecision) {
6763
+ logger.error(`plugin "${entry.plugin}" hook "${hook}" returned an invalid decision:`, message);
6764
+ } else {
6765
+ logger.warn(`plugin "${entry.plugin}" hook "${hook}" failed:`, message);
6766
+ }
6679
6767
  emitPluginEvent(entry, hook, {
6680
6768
  phase: timedOut ? "timeout" : "error",
6681
6769
  error: { message },
@@ -6687,8 +6775,9 @@ function createPluginHost(plugins = [], options = {}) {
6687
6775
  durationMs: performanceNow() - startedAt
6688
6776
  });
6689
6777
  if (entry.failureMode === "closed") {
6778
+ const cause = timedOut ? "timed out" : badDecision ? "invalid decision" : "hook error";
6690
6779
  result.decision = "deny";
6691
- result.reason = `${entry.plugin}: ${timedOut ? "timed out" : "hook error"} (fail-closed) \u2014 ${message}`;
6780
+ result.reason = `${entry.plugin}: ${cause} (fail-closed) \u2014 ${message}`;
6692
6781
  result.deniedBy = entry.plugin;
6693
6782
  result.input = current;
6694
6783
  return result;
@@ -6746,11 +6835,19 @@ function createPluginHost(plugins = [], options = {}) {
6746
6835
  if (output.metadata && typeof output.metadata === "object") {
6747
6836
  result.metadata = { ...result.metadata ?? {}, ...output.metadata };
6748
6837
  }
6749
- const decision = output.decision;
6838
+ let decision = output.decision;
6839
+ if (decision === "ask" && hook !== "preToolUse") {
6840
+ logger.warn(
6841
+ `plugin "${entry.plugin}" returned "ask" from "${hook}", which cannot pause for approval; treating it as "deny".`
6842
+ );
6843
+ decision = "deny";
6844
+ }
6750
6845
  const escalated = decision ? escalate(result.decision, decision) : result.decision;
6751
6846
  const escalatesHere = escalated !== result.decision;
6752
6847
  result.decision = escalated;
6753
- if (typeof output.reason === "string" && (escalatesHere || !result.reason)) {
6848
+ if (escalatesHere) {
6849
+ result.reason = typeof output.reason === "string" ? `${entry.plugin}: ${output.reason}` : void 0;
6850
+ } else if (typeof output.reason === "string" && !result.reason) {
6754
6851
  result.reason = `${entry.plugin}: ${output.reason}`;
6755
6852
  }
6756
6853
  if (mutatedHere) {
@@ -6844,7 +6941,7 @@ async function openPluginSession(runHost, params) {
6844
6941
  const appended = startGate.collected.systemPromptAppend ?? [];
6845
6942
  const systemPromptAppend = appended.length > 0 ? appended.join("\n\n") : void 0;
6846
6943
  const last = messages[messages.length - 1];
6847
- if (params.resumed || last?.role !== "user" || !runHost.has("userPromptSubmit")) {
6944
+ if (params.pausedMidRun || last?.role !== "user" || !runHost.has("userPromptSubmit")) {
6848
6945
  return { messages, systemPromptAppend };
6849
6946
  }
6850
6947
  const content = last.content;
@@ -7089,6 +7186,11 @@ function createAgent(opts) {
7089
7186
  delete nextCtx.__paused;
7090
7187
  state = { ...state, ctx: Object.keys(nextCtx).length > 0 ? nextCtx : void 0 };
7091
7188
  }
7189
+ if (state.ctx?.__restoredFromSnapshot) {
7190
+ const nextCtx = { ...state.ctx };
7191
+ delete nextCtx.__restoredFromSnapshot;
7192
+ state = { ...state, ctx: Object.keys(nextCtx).length > 0 ? nextCtx : void 0 };
7193
+ }
7092
7194
  let resumeStage = null;
7093
7195
  if (state.ctx?.__resumeStage) {
7094
7196
  const nextCtx = { ...state.ctx };
@@ -7363,7 +7465,7 @@ function createAgent(opts) {
7363
7465
  continue;
7364
7466
  }
7365
7467
  if (toolCalls.length === 0) {
7366
- if (soManager && !state.ctx?.__finalizedDueToStructuredOutput && !state.ctx?.__structuredOutputForceFinalize) {
7468
+ if (soManager && !state.ctx?.__finalizedDueToStructuredOutput && !state.ctx?.__structuredOutputForceFinalize && !state.ctx?.__guardrailBlocked) {
7367
7469
  if (soManager.strategy.kind === "native") {
7368
7470
  const assistantText = extractMessageText2(lastMsg);
7369
7471
  if (assistantText) {
@@ -7532,6 +7634,7 @@ function createAgent(opts) {
7532
7634
  delete ctx.__plugins;
7533
7635
  delete ctx.__applySystemPromptContribution;
7534
7636
  delete ctx.__pluginState;
7637
+ delete ctx.__guardrailBlocked;
7535
7638
  const inheritedSession = config?.__pluginSession;
7536
7639
  ctx.__runId = inheritedSession?.runId ?? traceSession?.sessionId ?? `run_${nanoid.nanoid(10)}`;
7537
7640
  const stateHolder = inheritedSession?.stateHolder ?? { value: void 0 };
@@ -7581,9 +7684,11 @@ function createAgent(opts) {
7581
7684
  usage: input.usage || { perRequest: [], totals: {} }
7582
7685
  };
7583
7686
  stateHolder.value = initial;
7584
- const resumed = Boolean(
7585
- input.ctx?.__restoredFromSnapshot || input.ctx?.__paused || input.ctx?.__resumeStage || input.ctx?.__awaitingApproval || input.ctx?.__awaitingUserQuestion
7687
+ const pausedMidRun = Boolean(
7688
+ input.ctx?.__paused || input.ctx?.__resumeStage || input.ctx?.__awaitingApproval || input.ctx?.__awaitingUserQuestion
7586
7689
  );
7690
+ const resumed = Boolean(input.ctx?.__restoredFromSnapshot || pausedMidRun);
7691
+ delete ctx.__restoredFromSnapshot;
7587
7692
  const startedAt = Date.now();
7588
7693
  let sessionEnded = false;
7589
7694
  const endSession = async (status, payload) => {
@@ -7609,6 +7714,7 @@ function createAgent(opts) {
7609
7714
  const opened = await openPluginSession(runHost, {
7610
7715
  messages: initial.messages,
7611
7716
  resumed,
7717
+ pausedMidRun,
7612
7718
  config
7613
7719
  });
7614
7720
  if (opened.messages !== initial.messages) {
@@ -7969,6 +8075,10 @@ function collectRecentTurns(messages, lastTurnsToKeep) {
7969
8075
  function renderStructuredSummary(summary) {
7970
8076
  if (!summary) return "";
7971
8077
  const lines = ["Context summary:"];
8078
+ if (summary.user_directives && summary.user_directives.length > 0) {
8079
+ lines.push("User directives (still in force \u2014 follow them):");
8080
+ lines.push(...summary.user_directives.map((directive) => `- ${directive}`));
8081
+ }
7972
8082
  if (summary.stable_facts.length > 0) {
7973
8083
  lines.push("Stable facts:");
7974
8084
  lines.push(...summary.stable_facts.map((fact) => `- ${fact.key}: ${fact.value}`));
@@ -8044,17 +8154,46 @@ function clampToBudget(messages, maxContextTokens) {
8044
8154
  }
8045
8155
  return working;
8046
8156
  }
8157
+ function collectSummaryOnlyBody(body, hasSummary, lastTurnsToKeep) {
8158
+ if (!hasSummary) {
8159
+ return collectRecentTurns(body, Math.max(1, lastTurnsToKeep));
8160
+ }
8161
+ let boundary = -1;
8162
+ for (let index = body.length - 1; index >= 0; index -= 1) {
8163
+ if (isSyntheticSummaryMessage(body[index])) {
8164
+ boundary = index;
8165
+ break;
8166
+ }
8167
+ }
8168
+ let lastUserIndex = -1;
8169
+ for (let index = body.length - 1; index >= 0; index -= 1) {
8170
+ if (body[index].role === "user") {
8171
+ lastUserIndex = index;
8172
+ break;
8173
+ }
8174
+ }
8175
+ const keep = /* @__PURE__ */ new Set();
8176
+ const firstUserIndex = body.findIndex((message) => message.role === "user");
8177
+ if (firstUserIndex >= 0) keep.add(firstUserIndex);
8178
+ const verbatimFrom = Math.min(lastUserIndex >= 0 ? lastUserIndex : body.length, boundary + 1);
8179
+ for (let index = Math.max(0, verbatimFrom); index < body.length; index += 1) {
8180
+ if (!isSyntheticSummaryMessage(body[index])) keep.add(index);
8181
+ }
8182
+ return body.filter((_, index) => keep.has(index));
8183
+ }
8047
8184
  function buildModelMessages(state, config) {
8048
8185
  const rawMessages = state.messages || [];
8049
8186
  if (config.context.policy === "raw") {
8050
8187
  return clampToBudget(rawMessages, config.limits.maxContextTokens);
8051
8188
  }
8052
- const recentMessages = collectRecentTurns(rawMessages, config.context.lastTurnsToKeep);
8053
8189
  const latestSummary = state.summaryRecords?.[state.summaryRecords.length - 1];
8054
8190
  const summaryText = renderStructuredSummary(latestSummary);
8055
8191
  const memoryText = renderMemoryBlock(state.memoryFacts);
8056
- const systemMessage2 = recentMessages[0]?.role === "system" ? recentMessages[0] : void 0;
8057
- const body = systemMessage2 ? recentMessages.slice(1) : recentMessages;
8192
+ const systemMessage2 = rawMessages[0]?.role === "system" ? rawMessages[0] : void 0;
8193
+ const body = config.context.policy === "summary_only" ? collectSummaryOnlyBody(systemMessage2 ? rawMessages.slice(1) : rawMessages, Boolean(summaryText), config.context.lastTurnsToKeep) : (() => {
8194
+ const recentMessages = collectRecentTurns(rawMessages, config.context.lastTurnsToKeep);
8195
+ return recentMessages[0]?.role === "system" ? recentMessages.slice(1) : recentMessages;
8196
+ })();
8058
8197
  const syntheticContextMessages = [];
8059
8198
  if (summaryText) {
8060
8199
  syntheticContextMessages.push({ role: "system", name: "context_summary", content: summaryText });
@@ -8065,7 +8204,7 @@ function buildModelMessages(state, config) {
8065
8204
  const assembled = [
8066
8205
  ...systemMessage2 ? [systemMessage2] : [],
8067
8206
  ...syntheticContextMessages,
8068
- ...config.context.policy === "summary_only" ? body.filter((message) => message.role === "user" || message.role === "assistant").slice(-2) : body
8207
+ ...body
8069
8208
  ];
8070
8209
  return clampToBudget(assembled, config.limits.maxContextTokens);
8071
8210
  }
@@ -8110,6 +8249,7 @@ function extractJsonObject(text) {
8110
8249
  }
8111
8250
  function normalizeStructuredSummary(input, fallbackText) {
8112
8251
  const summary = {
8252
+ user_directives: Array.isArray(input?.user_directives) ? input.user_directives.map((directive) => String(directive ?? "").trim()).filter(Boolean) : [],
8113
8253
  stable_facts: Array.isArray(input?.stable_facts) ? input.stable_facts.filter((fact) => fact && typeof fact === "object").map((fact) => ({
8114
8254
  key: String(fact.key || "unknown"),
8115
8255
  value: String(fact.value || ""),
@@ -8170,7 +8310,7 @@ function extractCanonicalFacts(messages) {
8170
8310
  return [...facts.values()];
8171
8311
  }
8172
8312
  function isSummarizedToolPlaceholder(content) {
8173
- return typeof content === "string" && (content === "SUMMARIZED" || content.startsWith("SUMMARIZED_TOOL_RESPONSE") || content.startsWith("ARCHIVED_TOOL_RESPONSE") || content.startsWith("STRUCTURED_TOOL_RESPONSE") || content.startsWith("DROPPED_TOOL_RESPONSE"));
8313
+ return isCompactedToolContent(content);
8174
8314
  }
8175
8315
  function collectLatestTurnToolCallIds(messages) {
8176
8316
  const ids = /* @__PURE__ */ new Set();
@@ -8229,11 +8369,16 @@ function runIntegrityCheck(previous, current) {
8229
8369
  const previousKeys = new Set((previous?.stable_facts || []).map((fact) => fact.key));
8230
8370
  const currentKeys = new Set(current.stable_facts.map((fact) => fact.key));
8231
8371
  const obsoleteKeys = new Set(current.discarded_obsolete);
8232
- const criticalFactLoss = [...previousKeys].some((key) => !obsoleteKeys.has(key) && !currentKeys.has(key));
8372
+ const currentDirectives = new Set(current.user_directives || []);
8373
+ const lostDirectives = (previous?.user_directives || []).filter((directive) => !currentDirectives.has(directive) && !obsoleteKeys.has(directive));
8374
+ const criticalFactLoss = [...previousKeys].some((key) => !obsoleteKeys.has(key) && !currentKeys.has(key)) || lostDirectives.length > 0;
8233
8375
  const obsoleteFactRevived = current.stable_facts.some((fact) => obsoleteKeys.has(fact.key));
8234
8376
  if (criticalFactLoss) {
8235
8377
  notes.push("Missing previously retained facts; merged forward during integrity repair.");
8236
8378
  }
8379
+ if (lostDirectives.length > 0) {
8380
+ notes.push(`Dropped ${lostDirectives.length} user directive(s); merged forward during integrity repair.`);
8381
+ }
8237
8382
  if (obsoleteFactRevived) {
8238
8383
  notes.push("Obsolete facts reappeared; removed during integrity repair.");
8239
8384
  }
@@ -8256,7 +8401,13 @@ function repairStructuredSummary(previous, current, integrity) {
8256
8401
  }
8257
8402
  }
8258
8403
  const filteredFacts = integrity.obsoleteFactRevived ? nextFacts.filter((fact) => !current.discarded_obsolete.includes(fact.key)) : nextFacts;
8259
- return { ...current, stable_facts: filteredFacts };
8404
+ const directives = [...current.user_directives || []];
8405
+ for (const directive of previous.user_directives || []) {
8406
+ if (!directives.includes(directive) && !current.discarded_obsolete.includes(directive)) {
8407
+ directives.push(directive);
8408
+ }
8409
+ }
8410
+ return { ...current, user_directives: directives, stable_facts: filteredFacts };
8260
8411
  }
8261
8412
  function createContextSummarizeNode(opts) {
8262
8413
  return async (state) => {
@@ -8330,6 +8481,7 @@ function createContextSummarizeNode(opts) {
8330
8481
 
8331
8482
  Return exactly one JSON object with this schema:
8332
8483
  {
8484
+ "user_directives": [string],
8333
8485
  "stable_facts": [{ "key": string, "value": string, "confidence": number }],
8334
8486
  "active_goals": [string],
8335
8487
  "open_questions": [string],
@@ -8338,6 +8490,7 @@ Return exactly one JSON object with this schema:
8338
8490
  }
8339
8491
 
8340
8492
  Rules:
8493
+ - user_directives: standing instructions the user gave about HOW to work or answer for the rest of the conversation (language, format, tone, scope, things to always or never do) that still apply. NOT the tasks or questions themselves \u2014 "read the logs" is a task (an active goal), "always answer in Turkish" is a directive. Copy each directive close to verbatim; never merge, soften or drop one. Carry forward every directive from the previous structured summary unless the user explicitly revoked it \u2014 then put its exact text in discarded_obsolete.
8341
8494
  - Keep stable_facts only for facts that future turns must remember.
8342
8495
  - Put invalidated or superseded fact keys in discarded_obsolete.
8343
8496
  - Preserve active_goals still relevant to the user.
@@ -8405,6 +8558,7 @@ ${canonicalFacts.length > 0 ? canonicalFacts.map((fact) => `- ${fact.key}: ${fac
8405
8558
  durationMs = Date.now() - startTime;
8406
8559
  structuredSummary = normalizeStructuredSummary(void 0, previousSummary || conversationText.slice(0, 800));
8407
8560
  structuredSummary = mergeStableFacts(structuredSummary, canonicalFacts);
8561
+ structuredSummary.user_directives = [...previousStructuredSummary?.user_directives || []];
8408
8562
  integrity = { passed: true, criticalFactLoss: false, obsoleteFactRevived: false, notes: ["Fallback summary generated locally."] };
8409
8563
  summaryText = renderStructuredSummary(structuredSummary);
8410
8564
  const errorEvent = {
@@ -10265,6 +10419,11 @@ ${pluginAppend}`;
10265
10419
  const instance = {
10266
10420
  invoke: async (input, config) => {
10267
10421
  await ensurePluginSetup();
10422
+ if (input?.ctx?.__guardrailBlocked !== void 0) {
10423
+ const clearedCtx = { ...input.ctx };
10424
+ delete clearedCtx.__guardrailBlocked;
10425
+ input = { ...input, ctx: clearedCtx };
10426
+ }
10268
10427
  const sessionHolder = { value: input };
10269
10428
  const emitSessionEvent = (event) => {
10270
10429
  try {
@@ -10315,12 +10474,20 @@ ${pluginAppend}`;
10315
10474
  stateHolder: sessionHolder
10316
10475
  }
10317
10476
  };
10318
- const resumed = Boolean(
10319
- input.ctx?.__restoredFromSnapshot || input.ctx?.__paused || input.ctx?.__resumeStage || input.ctx?.__awaitingApproval || input.ctx?.__awaitingUserQuestion
10477
+ const pausedMidRun = Boolean(
10478
+ input.ctx?.__paused || input.ctx?.__resumeStage || input.ctx?.__awaitingApproval || input.ctx?.__awaitingUserQuestion
10320
10479
  );
10480
+ const resumed = Boolean(input.ctx?.__restoredFromSnapshot || pausedMidRun);
10481
+ if (input.ctx?.__restoredFromSnapshot) {
10482
+ const consumedCtx = { ...input.ctx };
10483
+ delete consumedCtx.__restoredFromSnapshot;
10484
+ input = { ...input, ctx: consumedCtx };
10485
+ sessionHolder.value = input;
10486
+ }
10321
10487
  const opened = await openPluginSession(runHost, {
10322
10488
  messages: input.messages || [],
10323
10489
  resumed,
10490
+ pausedMidRun,
10324
10491
  config
10325
10492
  });
10326
10493
  if (opened.denied) {
@@ -11110,7 +11277,7 @@ var HOOKS = {
11110
11277
  sessionStart: { implemented: true },
11111
11278
  userPromptSubmit: {
11112
11279
  implemented: true,
11113
- notes: "Fires once per run for a new user turn. Skipped on resume, where the transcript tail is a tool result rather than a user message."
11280
+ notes: "Fires once per run whenever the transcript tail is a NEW user turn \u2014 including a turn appended to a state that was restored from a snapshot. Skipped on a genuine resume, where the tail is a tool result rather than a user message."
11114
11281
  },
11115
11282
  preModelCall: {
11116
11283
  implemented: true,
@@ -11123,7 +11290,7 @@ var HOOKS = {
11123
11290
  },
11124
11291
  postToolUse: {
11125
11292
  implemented: true,
11126
- notes: "Raw output, before compression and the hard cap. Also fires on a tool ERROR. Does NOT fire for a call that parks the run."
11293
+ notes: "Raw output, before compression and the hard cap. Also fires on a tool ERROR, on a preToolUse `result` short-circuit and on a tool-cache hit (`input.source` says which). Does NOT fire for a call that parks the run."
11127
11294
  },
11128
11295
  preCompact: { implemented: true },
11129
11296
  postCompact: { implemented: true },
@@ -12107,6 +12274,12 @@ function auditLog(config = {}) {
12107
12274
  return "[unserializable]";
12108
12275
  }
12109
12276
  };
12277
+ const renderDetail = (detail) => {
12278
+ if (!detail || typeof detail !== "object" || Array.isArray(detail) || !("args" in detail)) return detail;
12279
+ const { args, ...rest } = detail;
12280
+ const rendered = renderArgs(args);
12281
+ return rendered === void 0 ? rest : { ...rest, args: rendered };
12282
+ };
12110
12283
  return {
12111
12284
  name: config.name ?? "audit-log",
12112
12285
  // Lowest number: runs first, so denials by later plugins are still recorded
@@ -12135,7 +12308,7 @@ function auditLog(config = {}) {
12135
12308
  at: (/* @__PURE__ */ new Date()).toISOString(),
12136
12309
  kind: "notification",
12137
12310
  status: kind,
12138
- detail
12311
+ detail: renderDetail(detail)
12139
12312
  });
12140
12313
  },
12141
12314
  sessionEnd: async ({ status }, ctx) => {