@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.d.cts CHANGED
@@ -451,6 +451,13 @@ interface HookMap {
451
451
  error?: Error;
452
452
  durationMs: number;
453
453
  executionId: string;
454
+ /**
455
+ * Where `output` came from. `tool` is a real execution (or its error);
456
+ * `hook` is a `preToolUse.result` short-circuit; `cache` is a hit on the
457
+ * tool's own result cache — that value already passed this hook once,
458
+ * when it was stored, so a redactor sees it twice.
459
+ */
460
+ source?: "tool" | "hook" | "cache";
454
461
  };
455
462
  output: {
456
463
  /** Rewritten output — redaction, normalization, schema coercion. */
@@ -1515,6 +1522,13 @@ type SummaryFactItem = {
1515
1522
  source?: string;
1516
1523
  };
1517
1524
  type StructuredSummary = {
1525
+ /**
1526
+ * Standing instructions the user gave that still apply ("answer in Turkish",
1527
+ * "never restart prod"). Kept verbatim across summaries: a directive dropped
1528
+ * by a later summary counts as critical loss and is merged forward, unless
1529
+ * the user explicitly revoked it (listed in `discarded_obsolete`).
1530
+ */
1531
+ user_directives?: string[];
1518
1532
  stable_facts: SummaryFactItem[];
1519
1533
  active_goals: string[];
1520
1534
  open_questions: string[];
package/dist/index.d.ts CHANGED
@@ -451,6 +451,13 @@ interface HookMap {
451
451
  error?: Error;
452
452
  durationMs: number;
453
453
  executionId: string;
454
+ /**
455
+ * Where `output` came from. `tool` is a real execution (or its error);
456
+ * `hook` is a `preToolUse.result` short-circuit; `cache` is a hit on the
457
+ * tool's own result cache — that value already passed this hook once,
458
+ * when it was stored, so a redactor sees it twice.
459
+ */
460
+ source?: "tool" | "hook" | "cache";
454
461
  };
455
462
  output: {
456
463
  /** Rewritten output — redaction, normalization, schema coercion. */
@@ -1515,6 +1522,13 @@ type SummaryFactItem = {
1515
1522
  source?: string;
1516
1523
  };
1517
1524
  type StructuredSummary = {
1525
+ /**
1526
+ * Standing instructions the user gave that still apply ("answer in Turkish",
1527
+ * "never restart prod"). Kept verbatim across summaries: a directive dropped
1528
+ * by a later summary counts as critical loss and is merged forward, unless
1529
+ * the user explicitly revoked it (listed in `discarded_obsolete`).
1530
+ */
1531
+ user_directives?: string[];
1518
1532
  stable_facts: SummaryFactItem[];
1519
1533
  active_goals: string[];
1520
1534
  open_questions: string[];
package/dist/index.mjs CHANGED
@@ -4591,18 +4591,60 @@ function createToolsNode(initialTools, opts) {
4591
4591
  toolStateRef.__currentToolCallId = toolCallId;
4592
4592
  delete toolStateRef.__awaitingApproval;
4593
4593
  }
4594
- let hookAskedForApproval = false;
4595
- let hookApprovalPrompt;
4596
- let hookResult;
4597
- if (host?.has("preToolUse")) {
4594
+ const stableJson = (value) => {
4595
+ try {
4596
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
4597
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
4598
+ const keys = Object.keys(value).sort();
4599
+ return `{${keys.map((k) => JSON.stringify(k) + ":" + stableJson(value[k])).join(",")}}`;
4600
+ } catch {
4601
+ return String(value);
4602
+ }
4603
+ };
4604
+ const rejectByPolicy = (gateReason, gatedArgs) => {
4605
+ const reason = gateReason || `Tool call blocked by policy: ${toolName}`;
4606
+ push({ role: "tool", content: reason, tool_call_id: toolCallId, name: tc.name });
4607
+ toolHistory.push({
4608
+ executionId: nanoid(),
4609
+ toolName,
4610
+ args: gatedArgs,
4611
+ output: reason,
4612
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4613
+ tool_call_id: toolCallId,
4614
+ status: "rejected"
4615
+ });
4616
+ onEvent?.({ type: "tool_call", phase: "skipped", name: toolName, id: tc.id, args: gatedArgs, error: { message: reason } });
4617
+ recordTraceEvent(traceSession, {
4618
+ type: "tool_call",
4619
+ label: `Tool Denied - ${toolName}`,
4620
+ actor: { scope: "tool", name: toolName, role: "tool" },
4621
+ status: "skipped",
4622
+ toolDetails,
4623
+ toolExecutionId: toolCallId,
4624
+ sections: buildToolTraceSections({
4625
+ args: gatedArgs,
4626
+ executionId: toolCallId,
4627
+ output: reason,
4628
+ status: "skipped",
4629
+ summary: reason,
4630
+ toolDetails,
4631
+ toolName
4632
+ })
4633
+ });
4634
+ toolCount += 1;
4635
+ return { status: "error" };
4636
+ };
4637
+ const applyPreToolUseGate = async (candidate) => {
4638
+ if (!host?.has("preToolUse")) return { ok: true, args: candidate, ask: false };
4598
4639
  const gate = await host.runGate("preToolUse", {
4599
4640
  toolName,
4600
4641
  toolCallId,
4601
- args,
4642
+ args: candidate,
4602
4643
  tool,
4603
4644
  executionCount: countSuccessfulToolExecutions(toolHistory, toolName)
4604
4645
  });
4605
- if (gate.input.args !== args) {
4646
+ let next = candidate;
4647
+ if (gate.input.args !== candidate) {
4606
4648
  const revalidated = validateToolArgs(tool, gate.input.args);
4607
4649
  if (!revalidated.ok && resolved.toolResponses.schemaValidation === "strict") {
4608
4650
  const message = `Tool argument validation failed after a plugin rewrite for ${toolName}: ${revalidated.message}`;
@@ -4610,48 +4652,31 @@ function createToolsNode(initialTools, opts) {
4610
4652
  markToolFailure(message, toolName, toolCallId);
4611
4653
  onEvent?.({ type: "tool_call", phase: "error", name: toolName, id: tc.id, args: gate.input.args, error: { message } });
4612
4654
  toolCount += 1;
4613
- return { status: "error" };
4655
+ return { ok: false, outcome: { status: "error" } };
4614
4656
  }
4615
- args = revalidated.ok ? revalidated.value : gate.input.args;
4657
+ next = revalidated.ok ? revalidated.value : gate.input.args;
4616
4658
  }
4617
4659
  if (gate.decision === "deny") {
4618
- const reason = gate.reason || `Tool call blocked by policy: ${toolName}`;
4619
- push({ role: "tool", content: reason, tool_call_id: toolCallId, name: tc.name });
4620
- toolHistory.push({
4621
- executionId: nanoid(),
4622
- toolName,
4623
- args,
4624
- output: reason,
4625
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4626
- tool_call_id: toolCallId,
4627
- status: "rejected"
4628
- });
4629
- onEvent?.({ type: "tool_call", phase: "skipped", name: toolName, id: tc.id, args, error: { message: reason } });
4630
- recordTraceEvent(traceSession, {
4631
- type: "tool_call",
4632
- label: `Tool Denied - ${toolName}`,
4633
- actor: { scope: "tool", name: toolName, role: "tool" },
4634
- status: "skipped",
4635
- toolDetails,
4636
- toolExecutionId: toolCallId,
4637
- sections: buildToolTraceSections({
4638
- args,
4639
- executionId: toolCallId,
4640
- output: reason,
4641
- status: "skipped",
4642
- summary: reason,
4643
- toolDetails,
4644
- toolName
4645
- })
4646
- });
4647
- toolCount += 1;
4648
- return { status: "error" };
4649
- }
4650
- if (gate.decision === "ask") {
4651
- hookAskedForApproval = true;
4652
- hookApprovalPrompt = gate.approvalPrompt;
4660
+ return { ok: false, outcome: rejectByPolicy(gate.reason, next) };
4653
4661
  }
4654
- if (gate.shortCircuit !== void 0) hookResult = gate.shortCircuit;
4662
+ return {
4663
+ ok: true,
4664
+ args: next,
4665
+ ask: gate.decision === "ask",
4666
+ approvalPrompt: gate.approvalPrompt,
4667
+ result: gate.shortCircuit
4668
+ };
4669
+ };
4670
+ let hookAskedForApproval = false;
4671
+ let hookApprovalPrompt;
4672
+ let hookResult;
4673
+ {
4674
+ const gated = await applyPreToolUseGate(args);
4675
+ if (!gated.ok) return gated.outcome;
4676
+ args = gated.args;
4677
+ hookAskedForApproval = gated.ask;
4678
+ hookApprovalPrompt = gated.approvalPrompt;
4679
+ if (gated.result !== void 0) hookResult = gated.result;
4655
4680
  }
4656
4681
  const maxExecutionsPerRun = normalizeMaxExecutionsPerRun(tool.maxExecutionsPerRun);
4657
4682
  if (maxExecutionsPerRun !== null) {
@@ -4736,7 +4761,34 @@ function createToolsNode(initialTools, opts) {
4736
4761
  return { status: "rejected", approval: approvalEntry };
4737
4762
  }
4738
4763
  if (approvalEntry.status === "approved" && approvalEntry.approvedArgs !== void 0) {
4739
- args = approvalEntry.approvedArgs;
4764
+ const approvedArgs = approvalEntry.approvedArgs;
4765
+ if (stableJson(approvedArgs) !== stableJson(args)) {
4766
+ const settleRefused = (resolution) => {
4767
+ approvalEntry.metadata = { ...approvalEntry.metadata || {}, resolution };
4768
+ approvalEntry.status = "executed";
4769
+ approvalEntry.resolvedAt = (/* @__PURE__ */ new Date()).toISOString();
4770
+ pendingByCallId.set(toolCallId, approvalEntry);
4771
+ };
4772
+ const revalidated = validateToolArgs(tool, approvedArgs);
4773
+ if (!revalidated.ok && resolved.toolResponses.schemaValidation === "strict") {
4774
+ const message = `Tool argument validation failed for reviewer-edited arguments of ${toolName}: ${revalidated.message}`;
4775
+ push({ role: "tool", content: message, tool_call_id: toolCallId, name: tc.name });
4776
+ markToolFailure(message, toolName, toolCallId);
4777
+ settleRefused("invalid_args");
4778
+ onEvent?.({ type: "tool_call", phase: "error", name: toolName, id: tc.id, args: approvedArgs, error: { message } });
4779
+ toolCount += 1;
4780
+ return { status: "error", approval: approvalEntry };
4781
+ }
4782
+ const gated = await applyPreToolUseGate(revalidated.ok ? revalidated.value : approvedArgs);
4783
+ if (!gated.ok) {
4784
+ settleRefused("policy_denied");
4785
+ return { ...gated.outcome, approval: approvalEntry };
4786
+ }
4787
+ args = gated.args;
4788
+ if (gated.result !== void 0 && hookResult === void 0) hookResult = gated.result;
4789
+ } else {
4790
+ args = approvedArgs;
4791
+ }
4740
4792
  onEvent?.({ type: "tool_approval", status: "approved", id: approvalEntry.id, toolName: approvalEntry.toolName, toolCallId: approvalEntry.toolCallId, decidedBy: approvalEntry.decidedBy, comment: approvalEntry.comment });
4741
4793
  }
4742
4794
  }
@@ -4746,16 +4798,6 @@ function createToolsNode(initialTools, opts) {
4746
4798
  const cacheCfg = tool.cache;
4747
4799
  const cacheEnabled = cacheCfg === true || cacheCfg && typeof cacheCfg === "object";
4748
4800
  const cacheKeyFn = cacheEnabled && typeof cacheCfg === "object" ? cacheCfg.keyFn : void 0;
4749
- const stableJson = (value) => {
4750
- try {
4751
- if (value === null || typeof value !== "object") return JSON.stringify(value);
4752
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
4753
- const keys = Object.keys(value).sort();
4754
- return `{${keys.map((k) => JSON.stringify(k) + ":" + stableJson(value[k])).join(",")}}`;
4755
- } catch {
4756
- return String(value);
4757
- }
4758
- };
4759
4801
  const cacheKey = cacheEnabled ? `${toolName}|${cacheKeyFn ? cacheKeyFn(args) : stableJson(args)}` : void 0;
4760
4802
  const toolCache = state.toolCache = state.toolCache || {};
4761
4803
  let cachedHit = void 0;
@@ -4803,6 +4845,22 @@ function createToolsNode(initialTools, opts) {
4803
4845
  try {
4804
4846
  if (cachedHit !== void 0) {
4805
4847
  const executionId2 = nanoid();
4848
+ if (host?.has("postToolUse")) {
4849
+ const shortGate = await host.runGate("postToolUse", {
4850
+ toolName,
4851
+ toolCallId,
4852
+ args,
4853
+ output: cachedHit,
4854
+ durationMs: 0,
4855
+ executionId: executionId2,
4856
+ source: shortCircuitSource === "plugin" ? "hook" : "cache"
4857
+ });
4858
+ if (shortGate.decision === "deny") {
4859
+ cachedHit = shortGate.reason || `Tool output withheld by policy: ${toolName}`;
4860
+ } else if (shortGate.input.output !== cachedHit) {
4861
+ cachedHit = shortGate.input.output;
4862
+ }
4863
+ }
4806
4864
  const { output: compressedCachedHit, stats: cachedContextPilotStats } = maybeCompressToolOutput(toolName, cachedHit, executionId2);
4807
4865
  const responsePolicy2 = applyToolResponseHardCap(toolName, compressedCachedHit, executionId2, resolved);
4808
4866
  toolHistory.push({
@@ -4969,7 +5027,8 @@ function createToolsNode(initialTools, opts) {
4969
5027
  args,
4970
5028
  output,
4971
5029
  durationMs,
4972
- executionId
5030
+ executionId,
5031
+ source: "tool"
4973
5032
  });
4974
5033
  if (postGate.decision === "deny") {
4975
5034
  output = postGate.reason || `Tool output withheld by policy: ${toolName}`;
@@ -5064,7 +5123,8 @@ function createToolsNode(initialTools, opts) {
5064
5123
  output: message,
5065
5124
  error: error instanceof Error ? error : new Error(message),
5066
5125
  durationMs,
5067
- executionId
5126
+ executionId,
5127
+ source: "tool"
5068
5128
  });
5069
5129
  if (errorGate.decision === "deny") {
5070
5130
  message = errorGate.reason || `Tool error withheld by policy: ${toolName}`;
@@ -5740,7 +5800,14 @@ var DISALLOWED_CTX_KEYS = /* @__PURE__ */ new Set([
5740
5800
  // Session bookkeeping: a resumed run must open its OWN session, so these
5741
5801
  // must not travel in a snapshot.
5742
5802
  "__pluginRunStarted",
5743
- "__pluginRunEnded"
5803
+ "__pluginRunEnded",
5804
+ // Set by restoreSnapshot on the way OUT, consumed by the next invoke. Never
5805
+ // persisted: a snapshot of a restored run would otherwise mark every later
5806
+ // restore as "resumed" forever.
5807
+ "__restoredFromSnapshot",
5808
+ // A per-turn verdict. It describes the run that produced the snapshot, not
5809
+ // the run that will resume from it.
5810
+ "__guardrailBlocked"
5744
5811
  ]);
5745
5812
  var clone = (value) => {
5746
5813
  if (typeof globalThis.structuredClone === "function") {
@@ -6079,6 +6146,9 @@ function isSyntheticSummaryMessage(message) {
6079
6146
  }
6080
6147
  return false;
6081
6148
  }
6149
+ function isCompactedToolContent(content) {
6150
+ 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"));
6151
+ }
6082
6152
 
6083
6153
  // src/structuredOutput/manager.ts
6084
6154
  var StructuredOutputManager = class {
@@ -6364,6 +6434,7 @@ var SLOT_NAMES = [
6364
6434
  "contextBuilder"
6365
6435
  ];
6366
6436
  var DECISION_RANK = { allow: 0, ask: 1, deny: 2 };
6437
+ var isHookDecision = (value) => typeof value === "string" && Object.prototype.hasOwnProperty.call(DECISION_RANK, value);
6367
6438
  function escalate(current, next) {
6368
6439
  return DECISION_RANK[next] > DECISION_RANK[current] ? next : current;
6369
6440
  }
@@ -6373,6 +6444,15 @@ var HookTimeoutError = class extends Error {
6373
6444
  this.name = "HookTimeoutError";
6374
6445
  }
6375
6446
  };
6447
+ var InvalidHookDecisionError = class extends Error {
6448
+ constructor(plugin, hook, value) {
6449
+ const shown = typeof value === "string" ? JSON.stringify(value) : String(value);
6450
+ super(
6451
+ `plugin "${plugin}" hook "${hook}" returned an unknown decision ${shown}; expected "allow", "ask" or "deny"`
6452
+ );
6453
+ this.name = "InvalidHookDecisionError";
6454
+ }
6455
+ };
6376
6456
  function withTimeout(value, ms, label) {
6377
6457
  if (!ms || !Number.isFinite(ms) || ms <= 0) return Promise.resolve(value);
6378
6458
  return new Promise((resolve, reject) => {
@@ -6662,10 +6742,18 @@ function createPluginHost(plugins = [], options = {}) {
6662
6742
  `plugin "${entry.plugin}" hook "${hook}"`
6663
6743
  );
6664
6744
  output = raw ?? void 0;
6745
+ if (output && typeof output === "object" && output.decision !== void 0 && !isHookDecision(output.decision)) {
6746
+ throw new InvalidHookDecisionError(entry.plugin, hook, output.decision);
6747
+ }
6665
6748
  } catch (err) {
6666
6749
  const message = err instanceof Error ? err.message : String(err);
6667
6750
  const timedOut = err instanceof HookTimeoutError;
6668
- logger.warn(`plugin "${entry.plugin}" hook "${hook}" failed:`, message);
6751
+ const badDecision = err instanceof InvalidHookDecisionError;
6752
+ if (badDecision) {
6753
+ logger.error(`plugin "${entry.plugin}" hook "${hook}" returned an invalid decision:`, message);
6754
+ } else {
6755
+ logger.warn(`plugin "${entry.plugin}" hook "${hook}" failed:`, message);
6756
+ }
6669
6757
  emitPluginEvent(entry, hook, {
6670
6758
  phase: timedOut ? "timeout" : "error",
6671
6759
  error: { message },
@@ -6677,8 +6765,9 @@ function createPluginHost(plugins = [], options = {}) {
6677
6765
  durationMs: performanceNow() - startedAt
6678
6766
  });
6679
6767
  if (entry.failureMode === "closed") {
6768
+ const cause = timedOut ? "timed out" : badDecision ? "invalid decision" : "hook error";
6680
6769
  result.decision = "deny";
6681
- result.reason = `${entry.plugin}: ${timedOut ? "timed out" : "hook error"} (fail-closed) \u2014 ${message}`;
6770
+ result.reason = `${entry.plugin}: ${cause} (fail-closed) \u2014 ${message}`;
6682
6771
  result.deniedBy = entry.plugin;
6683
6772
  result.input = current;
6684
6773
  return result;
@@ -6736,11 +6825,19 @@ function createPluginHost(plugins = [], options = {}) {
6736
6825
  if (output.metadata && typeof output.metadata === "object") {
6737
6826
  result.metadata = { ...result.metadata ?? {}, ...output.metadata };
6738
6827
  }
6739
- const decision = output.decision;
6828
+ let decision = output.decision;
6829
+ if (decision === "ask" && hook !== "preToolUse") {
6830
+ logger.warn(
6831
+ `plugin "${entry.plugin}" returned "ask" from "${hook}", which cannot pause for approval; treating it as "deny".`
6832
+ );
6833
+ decision = "deny";
6834
+ }
6740
6835
  const escalated = decision ? escalate(result.decision, decision) : result.decision;
6741
6836
  const escalatesHere = escalated !== result.decision;
6742
6837
  result.decision = escalated;
6743
- if (typeof output.reason === "string" && (escalatesHere || !result.reason)) {
6838
+ if (escalatesHere) {
6839
+ result.reason = typeof output.reason === "string" ? `${entry.plugin}: ${output.reason}` : void 0;
6840
+ } else if (typeof output.reason === "string" && !result.reason) {
6744
6841
  result.reason = `${entry.plugin}: ${output.reason}`;
6745
6842
  }
6746
6843
  if (mutatedHere) {
@@ -6834,7 +6931,7 @@ async function openPluginSession(runHost, params) {
6834
6931
  const appended = startGate.collected.systemPromptAppend ?? [];
6835
6932
  const systemPromptAppend = appended.length > 0 ? appended.join("\n\n") : void 0;
6836
6933
  const last = messages[messages.length - 1];
6837
- if (params.resumed || last?.role !== "user" || !runHost.has("userPromptSubmit")) {
6934
+ if (params.pausedMidRun || last?.role !== "user" || !runHost.has("userPromptSubmit")) {
6838
6935
  return { messages, systemPromptAppend };
6839
6936
  }
6840
6937
  const content = last.content;
@@ -7079,6 +7176,11 @@ function createAgent(opts) {
7079
7176
  delete nextCtx.__paused;
7080
7177
  state = { ...state, ctx: Object.keys(nextCtx).length > 0 ? nextCtx : void 0 };
7081
7178
  }
7179
+ if (state.ctx?.__restoredFromSnapshot) {
7180
+ const nextCtx = { ...state.ctx };
7181
+ delete nextCtx.__restoredFromSnapshot;
7182
+ state = { ...state, ctx: Object.keys(nextCtx).length > 0 ? nextCtx : void 0 };
7183
+ }
7082
7184
  let resumeStage = null;
7083
7185
  if (state.ctx?.__resumeStage) {
7084
7186
  const nextCtx = { ...state.ctx };
@@ -7353,7 +7455,7 @@ function createAgent(opts) {
7353
7455
  continue;
7354
7456
  }
7355
7457
  if (toolCalls.length === 0) {
7356
- if (soManager && !state.ctx?.__finalizedDueToStructuredOutput && !state.ctx?.__structuredOutputForceFinalize) {
7458
+ if (soManager && !state.ctx?.__finalizedDueToStructuredOutput && !state.ctx?.__structuredOutputForceFinalize && !state.ctx?.__guardrailBlocked) {
7357
7459
  if (soManager.strategy.kind === "native") {
7358
7460
  const assistantText = extractMessageText2(lastMsg);
7359
7461
  if (assistantText) {
@@ -7522,6 +7624,7 @@ function createAgent(opts) {
7522
7624
  delete ctx.__plugins;
7523
7625
  delete ctx.__applySystemPromptContribution;
7524
7626
  delete ctx.__pluginState;
7627
+ delete ctx.__guardrailBlocked;
7525
7628
  const inheritedSession = config?.__pluginSession;
7526
7629
  ctx.__runId = inheritedSession?.runId ?? traceSession?.sessionId ?? `run_${nanoid(10)}`;
7527
7630
  const stateHolder = inheritedSession?.stateHolder ?? { value: void 0 };
@@ -7571,9 +7674,11 @@ function createAgent(opts) {
7571
7674
  usage: input.usage || { perRequest: [], totals: {} }
7572
7675
  };
7573
7676
  stateHolder.value = initial;
7574
- const resumed = Boolean(
7575
- input.ctx?.__restoredFromSnapshot || input.ctx?.__paused || input.ctx?.__resumeStage || input.ctx?.__awaitingApproval || input.ctx?.__awaitingUserQuestion
7677
+ const pausedMidRun = Boolean(
7678
+ input.ctx?.__paused || input.ctx?.__resumeStage || input.ctx?.__awaitingApproval || input.ctx?.__awaitingUserQuestion
7576
7679
  );
7680
+ const resumed = Boolean(input.ctx?.__restoredFromSnapshot || pausedMidRun);
7681
+ delete ctx.__restoredFromSnapshot;
7577
7682
  const startedAt = Date.now();
7578
7683
  let sessionEnded = false;
7579
7684
  const endSession = async (status, payload) => {
@@ -7599,6 +7704,7 @@ function createAgent(opts) {
7599
7704
  const opened = await openPluginSession(runHost, {
7600
7705
  messages: initial.messages,
7601
7706
  resumed,
7707
+ pausedMidRun,
7602
7708
  config
7603
7709
  });
7604
7710
  if (opened.messages !== initial.messages) {
@@ -7959,6 +8065,10 @@ function collectRecentTurns(messages, lastTurnsToKeep) {
7959
8065
  function renderStructuredSummary(summary) {
7960
8066
  if (!summary) return "";
7961
8067
  const lines = ["Context summary:"];
8068
+ if (summary.user_directives && summary.user_directives.length > 0) {
8069
+ lines.push("User directives (still in force \u2014 follow them):");
8070
+ lines.push(...summary.user_directives.map((directive) => `- ${directive}`));
8071
+ }
7962
8072
  if (summary.stable_facts.length > 0) {
7963
8073
  lines.push("Stable facts:");
7964
8074
  lines.push(...summary.stable_facts.map((fact) => `- ${fact.key}: ${fact.value}`));
@@ -8034,17 +8144,46 @@ function clampToBudget(messages, maxContextTokens) {
8034
8144
  }
8035
8145
  return working;
8036
8146
  }
8147
+ function collectSummaryOnlyBody(body, hasSummary, lastTurnsToKeep) {
8148
+ if (!hasSummary) {
8149
+ return collectRecentTurns(body, Math.max(1, lastTurnsToKeep));
8150
+ }
8151
+ let boundary = -1;
8152
+ for (let index = body.length - 1; index >= 0; index -= 1) {
8153
+ if (isSyntheticSummaryMessage(body[index])) {
8154
+ boundary = index;
8155
+ break;
8156
+ }
8157
+ }
8158
+ let lastUserIndex = -1;
8159
+ for (let index = body.length - 1; index >= 0; index -= 1) {
8160
+ if (body[index].role === "user") {
8161
+ lastUserIndex = index;
8162
+ break;
8163
+ }
8164
+ }
8165
+ const keep = /* @__PURE__ */ new Set();
8166
+ const firstUserIndex = body.findIndex((message) => message.role === "user");
8167
+ if (firstUserIndex >= 0) keep.add(firstUserIndex);
8168
+ const verbatimFrom = Math.min(lastUserIndex >= 0 ? lastUserIndex : body.length, boundary + 1);
8169
+ for (let index = Math.max(0, verbatimFrom); index < body.length; index += 1) {
8170
+ if (!isSyntheticSummaryMessage(body[index])) keep.add(index);
8171
+ }
8172
+ return body.filter((_, index) => keep.has(index));
8173
+ }
8037
8174
  function buildModelMessages(state, config) {
8038
8175
  const rawMessages = state.messages || [];
8039
8176
  if (config.context.policy === "raw") {
8040
8177
  return clampToBudget(rawMessages, config.limits.maxContextTokens);
8041
8178
  }
8042
- const recentMessages = collectRecentTurns(rawMessages, config.context.lastTurnsToKeep);
8043
8179
  const latestSummary = state.summaryRecords?.[state.summaryRecords.length - 1];
8044
8180
  const summaryText = renderStructuredSummary(latestSummary);
8045
8181
  const memoryText = renderMemoryBlock(state.memoryFacts);
8046
- const systemMessage2 = recentMessages[0]?.role === "system" ? recentMessages[0] : void 0;
8047
- const body = systemMessage2 ? recentMessages.slice(1) : recentMessages;
8182
+ const systemMessage2 = rawMessages[0]?.role === "system" ? rawMessages[0] : void 0;
8183
+ const body = config.context.policy === "summary_only" ? collectSummaryOnlyBody(systemMessage2 ? rawMessages.slice(1) : rawMessages, Boolean(summaryText), config.context.lastTurnsToKeep) : (() => {
8184
+ const recentMessages = collectRecentTurns(rawMessages, config.context.lastTurnsToKeep);
8185
+ return recentMessages[0]?.role === "system" ? recentMessages.slice(1) : recentMessages;
8186
+ })();
8048
8187
  const syntheticContextMessages = [];
8049
8188
  if (summaryText) {
8050
8189
  syntheticContextMessages.push({ role: "system", name: "context_summary", content: summaryText });
@@ -8055,7 +8194,7 @@ function buildModelMessages(state, config) {
8055
8194
  const assembled = [
8056
8195
  ...systemMessage2 ? [systemMessage2] : [],
8057
8196
  ...syntheticContextMessages,
8058
- ...config.context.policy === "summary_only" ? body.filter((message) => message.role === "user" || message.role === "assistant").slice(-2) : body
8197
+ ...body
8059
8198
  ];
8060
8199
  return clampToBudget(assembled, config.limits.maxContextTokens);
8061
8200
  }
@@ -8100,6 +8239,7 @@ function extractJsonObject(text) {
8100
8239
  }
8101
8240
  function normalizeStructuredSummary(input, fallbackText) {
8102
8241
  const summary = {
8242
+ user_directives: Array.isArray(input?.user_directives) ? input.user_directives.map((directive) => String(directive ?? "").trim()).filter(Boolean) : [],
8103
8243
  stable_facts: Array.isArray(input?.stable_facts) ? input.stable_facts.filter((fact) => fact && typeof fact === "object").map((fact) => ({
8104
8244
  key: String(fact.key || "unknown"),
8105
8245
  value: String(fact.value || ""),
@@ -8160,7 +8300,7 @@ function extractCanonicalFacts(messages) {
8160
8300
  return [...facts.values()];
8161
8301
  }
8162
8302
  function isSummarizedToolPlaceholder(content) {
8163
- 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"));
8303
+ return isCompactedToolContent(content);
8164
8304
  }
8165
8305
  function collectLatestTurnToolCallIds(messages) {
8166
8306
  const ids = /* @__PURE__ */ new Set();
@@ -8219,11 +8359,16 @@ function runIntegrityCheck(previous, current) {
8219
8359
  const previousKeys = new Set((previous?.stable_facts || []).map((fact) => fact.key));
8220
8360
  const currentKeys = new Set(current.stable_facts.map((fact) => fact.key));
8221
8361
  const obsoleteKeys = new Set(current.discarded_obsolete);
8222
- const criticalFactLoss = [...previousKeys].some((key) => !obsoleteKeys.has(key) && !currentKeys.has(key));
8362
+ const currentDirectives = new Set(current.user_directives || []);
8363
+ const lostDirectives = (previous?.user_directives || []).filter((directive) => !currentDirectives.has(directive) && !obsoleteKeys.has(directive));
8364
+ const criticalFactLoss = [...previousKeys].some((key) => !obsoleteKeys.has(key) && !currentKeys.has(key)) || lostDirectives.length > 0;
8223
8365
  const obsoleteFactRevived = current.stable_facts.some((fact) => obsoleteKeys.has(fact.key));
8224
8366
  if (criticalFactLoss) {
8225
8367
  notes.push("Missing previously retained facts; merged forward during integrity repair.");
8226
8368
  }
8369
+ if (lostDirectives.length > 0) {
8370
+ notes.push(`Dropped ${lostDirectives.length} user directive(s); merged forward during integrity repair.`);
8371
+ }
8227
8372
  if (obsoleteFactRevived) {
8228
8373
  notes.push("Obsolete facts reappeared; removed during integrity repair.");
8229
8374
  }
@@ -8246,7 +8391,13 @@ function repairStructuredSummary(previous, current, integrity) {
8246
8391
  }
8247
8392
  }
8248
8393
  const filteredFacts = integrity.obsoleteFactRevived ? nextFacts.filter((fact) => !current.discarded_obsolete.includes(fact.key)) : nextFacts;
8249
- return { ...current, stable_facts: filteredFacts };
8394
+ const directives = [...current.user_directives || []];
8395
+ for (const directive of previous.user_directives || []) {
8396
+ if (!directives.includes(directive) && !current.discarded_obsolete.includes(directive)) {
8397
+ directives.push(directive);
8398
+ }
8399
+ }
8400
+ return { ...current, user_directives: directives, stable_facts: filteredFacts };
8250
8401
  }
8251
8402
  function createContextSummarizeNode(opts) {
8252
8403
  return async (state) => {
@@ -8320,6 +8471,7 @@ function createContextSummarizeNode(opts) {
8320
8471
 
8321
8472
  Return exactly one JSON object with this schema:
8322
8473
  {
8474
+ "user_directives": [string],
8323
8475
  "stable_facts": [{ "key": string, "value": string, "confidence": number }],
8324
8476
  "active_goals": [string],
8325
8477
  "open_questions": [string],
@@ -8328,6 +8480,7 @@ Return exactly one JSON object with this schema:
8328
8480
  }
8329
8481
 
8330
8482
  Rules:
8483
+ - 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.
8331
8484
  - Keep stable_facts only for facts that future turns must remember.
8332
8485
  - Put invalidated or superseded fact keys in discarded_obsolete.
8333
8486
  - Preserve active_goals still relevant to the user.
@@ -8395,6 +8548,7 @@ ${canonicalFacts.length > 0 ? canonicalFacts.map((fact) => `- ${fact.key}: ${fac
8395
8548
  durationMs = Date.now() - startTime;
8396
8549
  structuredSummary = normalizeStructuredSummary(void 0, previousSummary || conversationText.slice(0, 800));
8397
8550
  structuredSummary = mergeStableFacts(structuredSummary, canonicalFacts);
8551
+ structuredSummary.user_directives = [...previousStructuredSummary?.user_directives || []];
8398
8552
  integrity = { passed: true, criticalFactLoss: false, obsoleteFactRevived: false, notes: ["Fallback summary generated locally."] };
8399
8553
  summaryText = renderStructuredSummary(structuredSummary);
8400
8554
  const errorEvent = {
@@ -10255,6 +10409,11 @@ ${pluginAppend}`;
10255
10409
  const instance = {
10256
10410
  invoke: async (input, config) => {
10257
10411
  await ensurePluginSetup();
10412
+ if (input?.ctx?.__guardrailBlocked !== void 0) {
10413
+ const clearedCtx = { ...input.ctx };
10414
+ delete clearedCtx.__guardrailBlocked;
10415
+ input = { ...input, ctx: clearedCtx };
10416
+ }
10258
10417
  const sessionHolder = { value: input };
10259
10418
  const emitSessionEvent = (event) => {
10260
10419
  try {
@@ -10305,12 +10464,20 @@ ${pluginAppend}`;
10305
10464
  stateHolder: sessionHolder
10306
10465
  }
10307
10466
  };
10308
- const resumed = Boolean(
10309
- input.ctx?.__restoredFromSnapshot || input.ctx?.__paused || input.ctx?.__resumeStage || input.ctx?.__awaitingApproval || input.ctx?.__awaitingUserQuestion
10467
+ const pausedMidRun = Boolean(
10468
+ input.ctx?.__paused || input.ctx?.__resumeStage || input.ctx?.__awaitingApproval || input.ctx?.__awaitingUserQuestion
10310
10469
  );
10470
+ const resumed = Boolean(input.ctx?.__restoredFromSnapshot || pausedMidRun);
10471
+ if (input.ctx?.__restoredFromSnapshot) {
10472
+ const consumedCtx = { ...input.ctx };
10473
+ delete consumedCtx.__restoredFromSnapshot;
10474
+ input = { ...input, ctx: consumedCtx };
10475
+ sessionHolder.value = input;
10476
+ }
10311
10477
  const opened = await openPluginSession(runHost, {
10312
10478
  messages: input.messages || [],
10313
10479
  resumed,
10480
+ pausedMidRun,
10314
10481
  config
10315
10482
  });
10316
10483
  if (opened.denied) {
@@ -11100,7 +11267,7 @@ var HOOKS = {
11100
11267
  sessionStart: { implemented: true },
11101
11268
  userPromptSubmit: {
11102
11269
  implemented: true,
11103
- 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."
11270
+ 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."
11104
11271
  },
11105
11272
  preModelCall: {
11106
11273
  implemented: true,
@@ -11113,7 +11280,7 @@ var HOOKS = {
11113
11280
  },
11114
11281
  postToolUse: {
11115
11282
  implemented: true,
11116
- 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."
11283
+ 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."
11117
11284
  },
11118
11285
  preCompact: { implemented: true },
11119
11286
  postCompact: { implemented: true },
@@ -12097,6 +12264,12 @@ function auditLog(config = {}) {
12097
12264
  return "[unserializable]";
12098
12265
  }
12099
12266
  };
12267
+ const renderDetail = (detail) => {
12268
+ if (!detail || typeof detail !== "object" || Array.isArray(detail) || !("args" in detail)) return detail;
12269
+ const { args, ...rest } = detail;
12270
+ const rendered = renderArgs(args);
12271
+ return rendered === void 0 ? rest : { ...rest, args: rendered };
12272
+ };
12100
12273
  return {
12101
12274
  name: config.name ?? "audit-log",
12102
12275
  // Lowest number: runs first, so denials by later plugins are still recorded
@@ -12125,7 +12298,7 @@ function auditLog(config = {}) {
12125
12298
  at: (/* @__PURE__ */ new Date()).toISOString(),
12126
12299
  kind: "notification",
12127
12300
  status: kind,
12128
- detail
12301
+ detail: renderDetail(detail)
12129
12302
  });
12130
12303
  },
12131
12304
  sessionEnd: async ({ status }, ctx) => {