@corenel/harness 0.13.0 → 0.15.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.
Files changed (145) hide show
  1. package/dist/approvals/service.d.ts.map +1 -1
  2. package/dist/approvals/service.js +4 -1
  3. package/dist/approvals/service.js.map +1 -1
  4. package/dist/audit/log.d.ts.map +1 -1
  5. package/dist/audit/log.js +12 -1
  6. package/dist/audit/log.js.map +1 -1
  7. package/dist/audit/recorder.d.ts +12 -1
  8. package/dist/audit/recorder.d.ts.map +1 -1
  9. package/dist/audit/recorder.js +36 -5
  10. package/dist/audit/recorder.js.map +1 -1
  11. package/dist/audit/runAudit.d.ts +2 -1
  12. package/dist/audit/runAudit.d.ts.map +1 -1
  13. package/dist/audit/runAudit.js +4 -0
  14. package/dist/audit/runAudit.js.map +1 -1
  15. package/dist/conversation.d.ts +33 -0
  16. package/dist/conversation.d.ts.map +1 -1
  17. package/dist/conversation.js +44 -1
  18. package/dist/conversation.js.map +1 -1
  19. package/dist/core/compactHistory.d.ts +27 -18
  20. package/dist/core/compactHistory.d.ts.map +1 -1
  21. package/dist/core/compactHistory.js +106 -40
  22. package/dist/core/compactHistory.js.map +1 -1
  23. package/dist/core/dontsRecent.d.ts +5 -0
  24. package/dist/core/dontsRecent.d.ts.map +1 -0
  25. package/dist/core/dontsRecent.js +23 -0
  26. package/dist/core/dontsRecent.js.map +1 -0
  27. package/dist/core/foldBlocks.d.ts +43 -0
  28. package/dist/core/foldBlocks.d.ts.map +1 -0
  29. package/dist/core/foldBlocks.js +114 -0
  30. package/dist/core/foldBlocks.js.map +1 -0
  31. package/dist/core/foldTypes.d.ts +25 -0
  32. package/dist/core/foldTypes.d.ts.map +1 -0
  33. package/dist/core/foldTypes.js +2 -0
  34. package/dist/core/foldTypes.js.map +1 -0
  35. package/dist/core/loop.d.ts +26 -5
  36. package/dist/core/loop.d.ts.map +1 -1
  37. package/dist/core/loop.js +274 -74
  38. package/dist/core/loop.js.map +1 -1
  39. package/dist/core/recovery.d.ts +1 -0
  40. package/dist/core/recovery.d.ts.map +1 -1
  41. package/dist/core/recovery.js +12 -0
  42. package/dist/core/recovery.js.map +1 -1
  43. package/dist/core/spawn.d.ts +13 -1
  44. package/dist/core/spawn.d.ts.map +1 -1
  45. package/dist/core/spawn.js +10 -1
  46. package/dist/core/spawn.js.map +1 -1
  47. package/dist/core/summarize.d.ts +9 -3
  48. package/dist/core/summarize.d.ts.map +1 -1
  49. package/dist/core/summarize.js +97 -42
  50. package/dist/core/summarize.js.map +1 -1
  51. package/dist/dag/executeNode.d.ts +3 -1
  52. package/dist/dag/executeNode.d.ts.map +1 -1
  53. package/dist/dag/executeNode.js +1 -0
  54. package/dist/dag/executeNode.js.map +1 -1
  55. package/dist/guardrail/permission-service.d.ts.map +1 -1
  56. package/dist/guardrail/permission-service.js +31 -11
  57. package/dist/guardrail/permission-service.js.map +1 -1
  58. package/dist/guardrail/serialize.d.ts.map +1 -1
  59. package/dist/guardrail/serialize.js +5 -0
  60. package/dist/guardrail/serialize.js.map +1 -1
  61. package/dist/guardrail/validate.d.ts.map +1 -1
  62. package/dist/guardrail/validate.js +21 -6
  63. package/dist/guardrail/validate.js.map +1 -1
  64. package/dist/memory/store.d.ts +3 -0
  65. package/dist/memory/store.d.ts.map +1 -1
  66. package/dist/memory/store.js +16 -1
  67. package/dist/memory/store.js.map +1 -1
  68. package/dist/orchestrator/record.d.ts +7 -0
  69. package/dist/orchestrator/record.d.ts.map +1 -1
  70. package/dist/orchestrator/record.js +14 -2
  71. package/dist/orchestrator/record.js.map +1 -1
  72. package/dist/prompts/library/defaults.generated.d.ts +2 -0
  73. package/dist/prompts/library/defaults.generated.d.ts.map +1 -1
  74. package/dist/prompts/library/defaults.generated.js +6 -0
  75. package/dist/prompts/library/defaults.generated.js.map +1 -1
  76. package/dist/prompts/library/index.d.ts +3 -0
  77. package/dist/prompts/library/index.d.ts.map +1 -1
  78. package/dist/prompts/library/index.js +3 -0
  79. package/dist/prompts/library/index.js.map +1 -1
  80. package/dist/sessions/directivesStore.d.ts +9 -0
  81. package/dist/sessions/directivesStore.d.ts.map +1 -0
  82. package/dist/sessions/directivesStore.js +54 -0
  83. package/dist/sessions/directivesStore.js.map +1 -0
  84. package/dist/sessions/fileSessionStore.d.ts +3 -2
  85. package/dist/sessions/fileSessionStore.d.ts.map +1 -1
  86. package/dist/sessions/fileSessionStore.js +30 -9
  87. package/dist/sessions/fileSessionStore.js.map +1 -1
  88. package/dist/sessions/foldStorage.d.ts +15 -0
  89. package/dist/sessions/foldStorage.d.ts.map +1 -0
  90. package/dist/sessions/foldStorage.js +121 -0
  91. package/dist/sessions/foldStorage.js.map +1 -0
  92. package/dist/sessions/jsonl.d.ts +6 -0
  93. package/dist/sessions/jsonl.d.ts.map +1 -1
  94. package/dist/sessions/jsonl.js +15 -1
  95. package/dist/sessions/jsonl.js.map +1 -1
  96. package/dist/sidecar/accessTestRig.d.ts +32 -0
  97. package/dist/sidecar/accessTestRig.d.ts.map +1 -0
  98. package/dist/sidecar/accessTestRig.js +107 -0
  99. package/dist/sidecar/accessTestRig.js.map +1 -0
  100. package/dist/sidecar/askView.d.ts +20 -0
  101. package/dist/sidecar/askView.d.ts.map +1 -0
  102. package/dist/sidecar/askView.js +38 -0
  103. package/dist/sidecar/askView.js.map +1 -0
  104. package/dist/sidecar/host.d.ts +139 -11
  105. package/dist/sidecar/host.d.ts.map +1 -1
  106. package/dist/sidecar/host.js +394 -13
  107. package/dist/sidecar/host.js.map +1 -1
  108. package/dist/sidecar/hostCapability.d.ts +6 -0
  109. package/dist/sidecar/hostCapability.d.ts.map +1 -0
  110. package/dist/sidecar/hostCapability.js +7 -0
  111. package/dist/sidecar/hostCapability.js.map +1 -0
  112. package/dist/sidecar/identity.d.ts +5 -0
  113. package/dist/sidecar/identity.d.ts.map +1 -1
  114. package/dist/sidecar/memberGate.d.ts +5 -0
  115. package/dist/sidecar/memberGate.d.ts.map +1 -0
  116. package/dist/sidecar/memberGate.js +128 -0
  117. package/dist/sidecar/memberGate.js.map +1 -0
  118. package/dist/sidecar/server.d.ts +425 -11
  119. package/dist/sidecar/server.d.ts.map +1 -1
  120. package/dist/sidecar/server.js +1820 -53
  121. package/dist/sidecar/server.js.map +1 -1
  122. package/dist/skills.d.ts +7 -1
  123. package/dist/skills.d.ts.map +1 -1
  124. package/dist/skills.js +18 -5
  125. package/dist/skills.js.map +1 -1
  126. package/dist/tools/builtins.d.ts +17 -0
  127. package/dist/tools/builtins.d.ts.map +1 -1
  128. package/dist/tools/builtins.js +22 -2
  129. package/dist/tools/builtins.js.map +1 -1
  130. package/dist/tools/callAgent.d.ts +14 -1
  131. package/dist/tools/callAgent.d.ts.map +1 -1
  132. package/dist/tools/callAgent.js +18 -2
  133. package/dist/tools/callAgent.js.map +1 -1
  134. package/dist/tools/createCrewAgent.d.ts +39 -0
  135. package/dist/tools/createCrewAgent.d.ts.map +1 -0
  136. package/dist/tools/createCrewAgent.js +121 -0
  137. package/dist/tools/createCrewAgent.js.map +1 -0
  138. package/dist/tools/foldTools.d.ts +5 -0
  139. package/dist/tools/foldTools.d.ts.map +1 -0
  140. package/dist/tools/foldTools.js +123 -0
  141. package/dist/tools/foldTools.js.map +1 -0
  142. package/dist/tools/skillTool.d.ts.map +1 -1
  143. package/dist/tools/skillTool.js +7 -2
  144. package/dist/tools/skillTool.js.map +1 -1
  145. package/package.json +2 -2
@@ -3,9 +3,20 @@ import type { ChatCompletionMessageParam } from 'openai/resources/chat/completio
3
3
  import type { AgentTool, ToolContext, AgentEvent, Usage } from '@corenel/protocol';
4
4
  import { type RecoveryConfig } from './recovery';
5
5
  import type { Policy } from '@corenel/protocol';
6
+ import type { DontsJudge } from '@corenel/protocol';
6
7
  import type { PermissionService } from '../guardrail/permission-service';
7
8
  import { type CompressionConfig } from '../compression';
9
+ import { type FoldSummary } from './foldBlocks';
10
+ import type { FoldStorage } from './foldTypes';
8
11
  import { type SkillEntry } from '../tools/skillTool';
12
+ /** What the run ends with when even a folded history will not fit the window. */
13
+ export declare const OVERFLOW_GIVE_UP = "The conversation no longer fits this model's context window even after folding. Start a new chat or switch to a larger-context model.";
14
+ /** The overflow ending when there was nothing a fold could remove (compaction
15
+ * not configured, or nothing collapsible). */
16
+ export declare const OVERFLOW_NO_FOLD = "The conversation no longer fits this model's context window and there is nothing left to fold. Start a new chat or switch to a larger-context model.";
17
+ /** The overflow ending when a fold was possible but did not complete (the
18
+ * summarizer threw, for one). The loop appends the fold error and the 400. */
19
+ export declare const OVERFLOW_FOLD_FAILED = "The conversation no longer fits this model's context window and folding it failed. Try again, or start a new chat.";
9
20
  export interface RunAgentArgs {
10
21
  client: OpenAI;
11
22
  model: string;
@@ -38,6 +49,12 @@ export interface RunAgentArgs {
38
49
  /** The spawned child this run is, stamped onto every permission decision it
39
50
  * reports (`DecisionEvent.nodeId`). Absent for a root run. */
40
51
  decisionNodeId?: string;
52
+ /** Team don'ts (spec 2026-10-07 Part F). Present only on a team daemon run
53
+ * of an agent with don'ts. While present, reply text is held (no
54
+ * `assistant-delta`) and checked before it is committed, and every mutating
55
+ * or owner-level tool call is checked after the permission gate allows it.
56
+ * Absent: the loop is exactly as before. */
57
+ donts?: DontsJudge;
41
58
  /** Customize the tool-result text a refused tool call gets. The kernel is
42
59
  * host-agnostic and cannot know WHY a decision came back "deny" — a real
43
60
  * person declining an interactive prompt, and a non-interactive host that
@@ -54,13 +71,17 @@ export interface RunAgentArgs {
54
71
  * then behaves exactly as it did before this existed, which is to say it
55
72
  * runs until the provider rejects an oversized request with a fatal 400.
56
73
  * `summarize` is a host concern because it costs a model call; the host also
57
- * decides where `onCheckpoint` persists the body (a file the user can edit,
58
- * not an invisible blob). */
74
+ * decides, through `storage`, where a fold is persisted (files the user can
75
+ * edit, not an invisible blob). */
59
76
  compaction?: {
60
- summarize: (collapsed: ChatCompletionMessageParam[]) => Promise<string>;
61
- anchor?: (user: ChatCompletionMessageParam, answered: ChatCompletionMessageParam | null) => Promise<string>;
62
- onCheckpoint?: (checkpoint: string) => void | Promise<void>;
77
+ summarize: (collapsed: ChatCompletionMessageParam[]) => Promise<FoldSummary>;
78
+ /** Where folds are recorded and what backs recall_history/update_directives.
79
+ * Absent: the run still folds, it just records nothing. */
80
+ storage?: FoldStorage;
63
81
  };
82
+ /** Fold before the first request regardless of fill: set when the chat's own
83
+ * model is gone and the fallback's window cannot hold the history. */
84
+ forceFoldFirstTurn?: boolean;
64
85
  /** Intra-run self-healing budget (stream-retry + tool-steer). Omit for defaults. */
65
86
  recovery?: Partial<RecoveryConfig>;
66
87
  /** Optional output-token ceiling forwarded to the model on every turn (reasoning
@@ -1 +1 @@
1
- {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../core/loop.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../core/loop.ts"],"names":[],"mappings":"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"}
package/dist/core/loop.js CHANGED
@@ -5,8 +5,12 @@ import { initialState, evaluateTurnStart, evaluateToolCall, evaluateCompletion }
5
5
  import { checkFeature } from '../guards.js';
6
6
  import { STANDARD_POLICY } from '../guardrail/builtins.js';
7
7
  import { toolAddress } from '../tools/namespaces.js';
8
+ import { DONTS_MAX_STRIKES, DONTS_STOPPED, DONTS_STOP_REASON, DONT_REPLY_TOO_LONG_NOTE, DONT_TOOL_BLOCKED_RESULT, blockedReplyNote, blockedToolNote, } from '@corenel/protocol';
9
+ import { recentForJudge } from './dontsRecent.js';
8
10
  import { effectiveThreshold, formatOffloadRef, isOffloadPointer } from '../compression.js';
9
- import { compactHistory, compactionDecision, estimateFill, isCompactionMarker } from './compactHistory.js';
11
+ import { FALLBACK_CONTEXT_WINDOW } from './foldBlocks.js';
12
+ import { afterCompaction, compactHistory, compactionDecision, estimateFill, isCompactionMarker } from './compactHistory.js';
13
+ import { makeRecallHistoryTool, makeUpdateDirectivesTool } from '../tools/foldTools.js';
10
14
  import { contextNote, contextWindowLine, initialPressureGate, pressureDecision } from './contextPressure.js';
11
15
  import { fillToolsMarker } from '../prompts/toolsMarker.js';
12
16
  import { createToolLoader, serverIndex } from '../tools/toolSearch.js';
@@ -14,6 +18,14 @@ import { createSkillTool, skillIndex } from '../tools/skillTool.js';
14
18
  import { crewToolGuidance } from '../tools/crewList.js';
15
19
  import { createTurnVerifier } from './turnVerify.js';
16
20
  import { existedBefore, reportFileChanges, FILE_CHANGE_TOOLS } from './fileChanges.js';
21
+ /** What the run ends with when even a folded history will not fit the window. */
22
+ export const OVERFLOW_GIVE_UP = "The conversation no longer fits this model's context window even after folding. Start a new chat or switch to a larger-context model.";
23
+ /** The overflow ending when there was nothing a fold could remove (compaction
24
+ * not configured, or nothing collapsible). */
25
+ export const OVERFLOW_NO_FOLD = "The conversation no longer fits this model's context window and there is nothing left to fold. Start a new chat or switch to a larger-context model.";
26
+ /** The overflow ending when a fold was possible but did not complete (the
27
+ * summarizer threw, for one). The loop appends the fold error and the 400. */
28
+ export const OVERFLOW_FOLD_FAILED = "The conversation no longer fits this model's context window and folding it failed. Try again, or start a new chat.";
17
29
  /** The most tools one request may carry.
18
30
  *
19
31
  * OpenAI-compatible providers refuse the whole request above this -- "Invalid
@@ -37,6 +49,18 @@ export async function runAgent(args) {
37
49
  const verifier = createTurnVerifier({ kv: toolCtx.kv, files: toolCtx.files });
38
50
  const policy = args.policy ?? STANDARD_POLICY;
39
51
  const permission = args.permission;
52
+ const donts = args.donts;
53
+ /** The resolved permission level of a call the don'ts judge checks: a
54
+ * mutating call, or one whose level is `owner`. Null for every other call,
55
+ * for an unknown tool, and always without the port. */
56
+ const dontsCheckedLevel = (tool) => {
57
+ if (!donts || !tool)
58
+ return null;
59
+ const level = permission
60
+ ? permission.resolveLevel({ name: tool.name, address: toolAddress(tool), mutates: tool.mutates, permission: tool.permission, roomSafe: tool.roomSafe })
61
+ : 'allow';
62
+ return tool.mutates === true || level === 'owner' ? level : null;
63
+ };
40
64
  const compression = args.compression;
41
65
  const ctxWindow = compression?.contextWindow;
42
66
  const compressionStrategy = compression?.strategy ?? 'eager';
@@ -101,13 +125,17 @@ export async function runAgent(args) {
101
125
  * appears is an exact count of what this run added -- maintained by nothing,
102
126
  * and therefore impossible for a later `push` site to get wrong. */
103
127
  const incoming = new Set(args.messages);
128
+ /* A blocked-reply note is a system message this run appended. It is counted,
129
+ * never mistaken for the system prompt the walk below stops at. Empty
130
+ * without the don'ts port. */
131
+ const dontsNotes = new Set();
104
132
  const appendedFromTail = () => {
105
133
  let n = 0;
106
134
  for (let i = messages.length - 1; i >= 0; i--) {
107
135
  const m = messages[i];
108
- // A compaction's own checkpoint/anchor, and the system prompt unshifted
136
+ // A fold's own summary/directives blocks, and the system prompt unshifted
109
137
  // below, are fresh objects too -- and neither is something this run said.
110
- if (incoming.has(m) || m.role === 'system' || isCompactionMarker(m))
138
+ if (incoming.has(m) || (m.role === 'system' && !dontsNotes.has(m)) || isCompactionMarker(m))
111
139
  break;
112
140
  n++;
113
141
  }
@@ -122,7 +150,14 @@ export async function runAgent(args) {
122
150
  ? createToolLoader({ deferred, loaded: args.loadedTools, onChange: (names) => emit({ type: 'tools-loaded', names }) })
123
151
  : null;
124
152
  const skills = args.skills ?? [];
125
- const eager = [...tools, ...(skills.length ? [createSkillTool(skills)] : []), ...(loader ? [loader.searchTool] : [])];
153
+ const storage = args.compaction?.storage;
154
+ const foldTools = [
155
+ ...(storage?.transcript ? [makeRecallHistoryTool(storage.transcript)] : []),
156
+ ...(storage?.directives ? [makeUpdateDirectivesTool(storage.directives)] : []),
157
+ ];
158
+ /* Fold tools LAST: when MAX_PROVIDER_TOOLS caps this list, they must never be
159
+ * what pushes `use_skill` or `tool_search` off the end. */
160
+ const eager = [...tools, ...(skills.length ? [createSkillTool(skills)] : []), ...(loader ? [loader.searchTool] : []), ...foldTools];
126
161
  const capped = (list) => (list.length > MAX_PROVIDER_TOOLS ? list.slice(0, MAX_PROVIDER_TOOLS) : list);
127
162
  const activeTools = () => capped(loader ? [...eager, ...loader.loaded()] : eager);
128
163
  let sent = activeTools();
@@ -197,6 +232,82 @@ export async function runAgent(args) {
197
232
  const known = new Map([...eager, ...deferred].map((t) => [t.name, t]));
198
233
  await compactIncomingOffloads(messages, known, compression, toolCtx.recall, emit);
199
234
  }
235
+ const foldWindow = ctxWindow || FALLBACK_CONTEXT_WINDOW;
236
+ let forceFold = !!args.forceFoldFirstTurn;
237
+ /* One fold, used by three triggers: the fill gate, a forced fold (model
238
+ * switch), and the overflow retry. Rebuilds `messages` in place -- callers
239
+ * (and `res.messages`) hold this array. Returns what happened; on 'failed'
240
+ * the reason is left in `foldError` for the overflow message.
241
+ *
242
+ * A summarizer failure is the same shape as a persist failure: the window is
243
+ * full, the model call meant to compress it did not come back, and the honest
244
+ * response is to carry on UNFOLDED rather than end the run on the recovery
245
+ * path. The turn proceeds with the history it had, which is exactly what
246
+ * would have happened without compaction at all. */
247
+ let foldError = '';
248
+ const fold = async (offloadFill) => {
249
+ if (!args.compaction)
250
+ return 'nothing';
251
+ try {
252
+ let override = null;
253
+ try {
254
+ override = (await storage?.directives?.read()) ?? null;
255
+ }
256
+ catch (e) {
257
+ alog('directives read failed', e instanceof Error ? e.message : String(e));
258
+ }
259
+ const out = await compactHistory(messages, {
260
+ summarize: args.compaction.summarize,
261
+ contextWindow: foldWindow,
262
+ directivesOverride: override,
263
+ recallAvailable: !!storage?.transcript,
264
+ offloadOverBytes: compression && compression.mode !== 'off'
265
+ ? effectiveThreshold(compression.mode, offloadFill)
266
+ : undefined,
267
+ offload: toolCtx.recall
268
+ // Recorded under 'user' rather than a tool name: the RecallStore is
269
+ // indifferent to who authored the bulk, but the transcript should not
270
+ // claim a tool produced what a person pasted.
271
+ ? async (content) => formatOffloadRef(await toolCtx.recall.record('user', content))
272
+ : undefined,
273
+ });
274
+ /* Nothing to collapse: no fold happened, so nothing is recorded or emitted. */
275
+ if (!out)
276
+ return 'nothing';
277
+ messages.splice(0, messages.length, ...out.messages);
278
+ /* The 'deferred' strategy holds ABSOLUTE indices into `messages`, to
279
+ * rewrite those entries once the model has seen them in full. The splice
280
+ * above just invalidated every one of them, and the rewrite pass read
281
+ * `.role` off the resulting undefined -- a crash in the interactive
282
+ * CLI's shipping configuration ('deferred' + compaction). Nothing is
283
+ * lost by dropping them: the tool results they point at were collapsed
284
+ * into the summary and are not in `messages` any more. */
285
+ deferredOffloads.length = 0;
286
+ /* The fold has ALREADY happened by this point. Failing to RECORD it must
287
+ * not take the run with it -- the checkpoint store deliberately rejects
288
+ * rather than risk clobbering an existing CHECKPOINT.md, and a read-only
289
+ * workspace rejects too. Dying here would lose the whole conversation in
290
+ * order to save nothing. */
291
+ try {
292
+ await storage?.onFold?.({ summary: out.summary, directives: out.directives, collapsed: out.collapsed });
293
+ }
294
+ catch (e) {
295
+ alog('fold persist failed', e instanceof Error ? e.message : String(e));
296
+ }
297
+ emit({ type: 'checkpoint', collapsed: out.collapsedCount, checkpoint: out.summary });
298
+ compactGate = afterCompaction();
299
+ return 'folded';
300
+ }
301
+ catch (e) {
302
+ /* Re-arm so a later turn may try again rather than giving up for good.
303
+ * The floor is left as it was: it describes where the last SUCCESSFUL
304
+ * fold landed, and a failed attempt moved nothing. */
305
+ compactGate = { ...compactGate, armed: true };
306
+ foldError = e instanceof Error ? e.message : String(e);
307
+ alog('compaction failed', foldError);
308
+ return 'failed';
309
+ }
310
+ };
200
311
  const usage = { promptTokens: 0, completionTokens: 0 };
201
312
  /** Model calls made, and the largest single request among them. */
202
313
  let calls = 0;
@@ -205,6 +316,9 @@ export async function runAgent(args) {
205
316
  let stopReason = 'stop';
206
317
  let lastToolSig = null;
207
318
  let stopNudges = 0;
319
+ /** Denials by the don'ts judge in this run; the third stops it. Counted per
320
+ * loop: a callee's own port counts its own (preflight F20). */
321
+ let strikes = 0;
208
322
  let lastStreamErr = ''; // the blip a stream retry recovered from (for the heal record)
209
323
  for (let turn = 1; unlimitedTurns || turn <= maxTurns; turn++) {
210
324
  if (signal.aborted) {
@@ -221,74 +335,26 @@ export async function runAgent(args) {
221
335
  emit({ type: 'injected', content: typeof m.content === 'string' ? m.content : '' });
222
336
  }
223
337
  }
224
- /* Rung-2 history compaction, BEFORE the request is built so the compacted
225
- * array is what goes on the wire. `fill` is last turn's prompt/window
226
- * ratio; shouldCompact refuses below the threshold and refuses again when
227
- * there is nothing collapsible, because a compaction that removes nothing
228
- * still rewrites the cached prefix and re-bills everything after it.
229
- * Rebuilt in place: callers (and `res.messages`) hold this array. */
230
- /* On turn 1 there is no usage reading yet, so `fill` is 0 and the gate
231
- * could not fire before the first request -- which is precisely how a run
232
- * resuming an already-oversized conversation put it on the wire and took
233
- * the fatal 400. Fall back to reading the window off the messages. Only as
234
- * a fallback: a real reading is always better than an estimate that cannot
235
- * see the tool schemas. */
338
+ /* Rung-2 fold, BEFORE the request is built so the folded array is what goes
339
+ * on the wire. `fill` is last turn's prompt/window ratio; the gate refuses
340
+ * below the threshold and refuses again when there is nothing collapsible,
341
+ * because a fold that removes nothing still rewrites the cached prefix and
342
+ * re-bills everything after it. On turn 1 there is no usage reading yet, so
343
+ * `fill` is 0 and the gate could not fire before the first request -- which
344
+ * is how a resumed, already-oversized conversation took the fatal 400. Fall
345
+ * back to reading the window off the messages; only as a fallback: a real
346
+ * reading beats an estimate that cannot see the tool schemas. */
236
347
  const gateFill = fill || estimateFill(messages, ctxWindow);
237
- const gate = compactionDecision(gateFill, messages, compactGate);
348
+ const prevGate = compactGate;
349
+ const gate = compactionDecision(gateFill, messages, compactGate, foldWindow);
238
350
  compactGate = gate.next;
239
- if (args.compaction && gate.compact) {
240
- const before = messages.length;
241
- /* A summarizer failure is the same shape as a persist failure: the window
242
- * is full, the model call meant to compress it did not come back, and the
243
- * honest response is to carry on UNCOMPACTED rather than end the run on
244
- * the recovery path. The turn then proceeds with the history it had,
245
- * which is exactly what would have happened without compaction at all. */
246
- try {
247
- const out = await compactHistory(messages, {
248
- summarize: args.compaction.summarize,
249
- anchor: args.compaction.anchor,
250
- offloadOverBytes: compression && compression.mode !== 'off'
251
- ? effectiveThreshold(compression.mode, gateFill)
252
- : undefined,
253
- offload: toolCtx.recall
254
- // Recorded under 'user' rather than a tool name: the RecallStore is
255
- // indifferent to who authored the bulk, but the transcript should not
256
- // claim a tool produced what a person pasted.
257
- ? async (content) => formatOffloadRef(await toolCtx.recall.record('user', content))
258
- : undefined,
259
- });
260
- messages.splice(0, messages.length, ...out.messages);
261
- /* The 'deferred' strategy holds ABSOLUTE indices into `messages`, to
262
- * rewrite those entries once the model has seen them in full. The splice
263
- * above just invalidated every one of them, and the rewrite pass read
264
- * `.role` off the resulting undefined -- a crash in the interactive
265
- * CLI's shipping configuration ('deferred' + compaction). Nothing is
266
- * lost by dropping them: the tool results they point at were collapsed
267
- * into the checkpoint and are not in `messages` any more. */
268
- deferredOffloads.length = 0;
269
- /* The compaction has ALREADY happened by this point. Failing to RECORD it
270
- * must not take the run with it -- checkpointStore.append deliberately
271
- * rejects rather than risk clobbering an existing CHECKPOINT.md, and a
272
- * read-only workspace rejects too. Dying here would lose the whole
273
- * conversation in order to save nothing. */
274
- try {
275
- await args.compaction.onCheckpoint?.(out.checkpoint);
276
- }
277
- catch (e) {
278
- alog('checkpoint persist failed', e instanceof Error ? e.message : String(e));
279
- }
280
- emit({ type: 'checkpoint', collapsed: before - messages.length, checkpoint: out.checkpoint });
281
- }
282
- catch (e) {
283
- /* Compaction failed (most likely the summarizer's own model call). The
284
- * turn proceeds with the history it had -- exactly what would have
285
- * happened had compaction never been configured. Re-arm so a later turn
286
- * may try again rather than giving up on the run for good. */
287
- /* The floor is left as it was: it describes where the last SUCCESSFUL
288
- * compaction landed, and a failed attempt moved nothing. */
289
- compactGate = { ...compactGate, armed: true };
290
- alog('compaction failed', e instanceof Error ? e.message : String(e));
291
- }
351
+ if (args.compaction && (gate.compact || forceFold)) {
352
+ forceFold = false;
353
+ /* The gate fired but there was nothing to fold (the only bulk is a
354
+ * pending tool call, held back): nothing was spent, so the latch is not
355
+ * either -- it fires again once there is something to collapse. */
356
+ if ((await fold(gateFill)) === 'nothing' && gate.compact)
357
+ compactGate = prevGate;
292
358
  }
293
359
  /* CONTEXT PRESSURE. After the compaction block, deliberately: if a
294
360
  * compaction just ran, the window it drained is the one the agent should
@@ -357,6 +423,8 @@ export async function runAgent(args) {
357
423
  turnAc = new AbortController();
358
424
  if (signal.aborted)
359
425
  turnAc.abort();
426
+ let overflowRetried = false;
427
+ let foldRetryOpen = false; // a 'retrying' fold-retry heal record awaits its outcome
360
428
  for (let sAttempt = 0;;) {
361
429
  try {
362
430
  const result = await streamCompletion(client, {
@@ -366,9 +434,16 @@ export async function runAgent(args) {
366
434
  tool_choice: toolDefs.length ? 'auto' : undefined,
367
435
  max_completion_tokens: args.maxCompletionTokens,
368
436
  temperature: args.temperature,
369
- }, turnAc.signal, (delta) => emit({ type: 'assistant-delta', delta }));
437
+ }, turnAc.signal,
438
+ // A held reply is never streamed: it is shown only once it passes.
439
+ (delta) => { if (!donts)
440
+ emit({ type: 'assistant-delta', delta }); });
370
441
  if (sAttempt > 0)
371
442
  emit({ type: 'heal', record: { site: 'stream', kind: 'transient', strategy: 'retry-backoff', attempt: sAttempt, outcome: 'recovered', error: lastStreamErr } });
443
+ if (foldRetryOpen) {
444
+ foldRetryOpen = false;
445
+ emit({ type: 'heal', record: { site: 'stream', kind: 'overflow', strategy: 'fold-retry', attempt: 1, outcome: 'recovered', error: lastStreamErr } });
446
+ }
372
447
  streamed = result;
373
448
  break;
374
449
  }
@@ -388,6 +463,26 @@ export async function runAgent(args) {
388
463
  const msg = String(e?.message || e);
389
464
  lastStreamErr = msg;
390
465
  const kind = classifyStreamError(msg);
466
+ /* A context-length rejection is not transient: re-sending the same
467
+ * history fails the same way. Fold once and re-send (`fold` rewrote
468
+ * `messages` in place and streamCompletion reads it by reference); a
469
+ * second overflow means folding could not make it fit. */
470
+ if (kind === 'overflow') {
471
+ const outcome = overflowRetried ? null : await fold(1);
472
+ if (outcome === 'folded') {
473
+ overflowRetried = true;
474
+ foldRetryOpen = true;
475
+ emit({ type: 'heal', record: { site: 'stream', kind, strategy: 'fold-retry', attempt: sAttempt + 1, outcome: 'retrying', error: msg } });
476
+ continue;
477
+ }
478
+ emit({ type: 'heal', record: { site: 'stream', kind, strategy: 'give-up', attempt: sAttempt + 1, outcome: 'abandoned', error: msg } });
479
+ const ending = overflowRetried ? `${OVERFLOW_GIVE_UP} (${msg})`
480
+ : outcome === 'failed' ? `${OVERFLOW_FOLD_FAILED} (${foldError}; ${msg})`
481
+ : `${OVERFLOW_NO_FOLD} (${msg})`;
482
+ emit({ type: 'error', message: ending });
483
+ streamFail = 'error';
484
+ break;
485
+ }
391
486
  sAttempt++;
392
487
  if (shouldRetryStream(kind, sAttempt, recovery)) {
393
488
  emit({ type: 'heal', record: { site: 'stream', kind, strategy: 'retry-backoff', attempt: sAttempt, outcome: 'retrying', error: msg } });
@@ -460,6 +555,46 @@ export async function runAgent(args) {
460
555
  compactGate = { armed: false, floor: fill };
461
556
  }
462
557
  emit({ type: 'usage', usage: streamed.usage, model });
558
+ /* TEAM DON'TS: the reply is checked before anything of this turn is
559
+ * committed, its tool calls included, so a denied reply runs none of them
560
+ * and leaves no tool_call without a result. A denied reply is never
561
+ * emitted or kept: the model gets a note instead and answers again. */
562
+ if (donts && streamed.content) {
563
+ emit({ type: 'donts-checking', subject: 'reply' });
564
+ const held = await donts.checkReply({ text: streamed.content, turn, recent: recentForJudge(messages) });
565
+ if (held.kind === 'cancelled') {
566
+ stopReason = 'aborted';
567
+ break;
568
+ }
569
+ if (held.kind === 'denied') {
570
+ strikes++;
571
+ if (strikes >= DONTS_MAX_STRIKES) {
572
+ emit({ type: 'error', message: DONTS_STOPPED });
573
+ stopReason = DONTS_STOP_REASON;
574
+ break;
575
+ }
576
+ const note = { role: 'system', content: held.tooLong ? DONT_REPLY_TOO_LONG_NOTE : blockedReplyNote(held.rule, held.by ?? 'nobody') };
577
+ dontsNotes.add(note);
578
+ messages.push(note);
579
+ if (!unlimitedTurns && turn === maxTurns)
580
+ stopReason = 'max_turns';
581
+ continue;
582
+ }
583
+ }
584
+ /* TEAM DON'TS (preflight F4 amended, F5): every call the judge will check,
585
+ * by id, with its resolved level. Such a call is committed with '{}' as its
586
+ * arguments and gets its real ones back only once it is announced (it
587
+ * passed, or a person allowed it), so a held excerpt is never stored on any
588
+ * exit path. The raw arguments stay in `streamed.toolCalls`, which is what
589
+ * runs and what the no-progress signature compares. Empty without the port. */
590
+ const dontsChecked = new Map();
591
+ if (donts) {
592
+ for (const t of streamed.toolCalls) {
593
+ const level = dontsCheckedLevel(toolByName.get(t.name));
594
+ if (level !== null)
595
+ dontsChecked.set(t.id, level);
596
+ }
597
+ }
463
598
  // Commit the assistant message to history (with tool_calls if any).
464
599
  const assistantMsg = streamed.toolCalls.length
465
600
  ? {
@@ -468,7 +603,7 @@ export async function runAgent(args) {
468
603
  tool_calls: streamed.toolCalls.map((t) => ({
469
604
  id: t.id,
470
605
  type: 'function',
471
- function: { name: t.name, arguments: t.arguments },
606
+ function: { name: t.name, arguments: dontsChecked.has(t.id) ? '{}' : t.arguments },
472
607
  })),
473
608
  }
474
609
  : { role: 'assistant', content: streamed.content };
@@ -503,18 +638,31 @@ export async function runAgent(args) {
503
638
  }
504
639
  lastToolSig = sig;
505
640
  let policyStop = false;
641
+ let dontsStop = false;
642
+ let dontsCancelled = false;
506
643
  for (const call of streamed.toolCalls) {
507
644
  let parsed = {};
508
645
  try {
509
646
  parsed = call.arguments ? JSON.parse(call.arguments) : {};
510
647
  }
511
648
  catch {
512
- messages.push({ role: 'tool', tool_call_id: call.id, content: `Error: arguments were not valid JSON: ${call.arguments}` });
649
+ // A checked call's arguments are held (preflight F4): never echoed back.
650
+ const invalid = dontsChecked.has(call.id)
651
+ ? DONT_INVALID_JSON_RESULT
652
+ : `Error: arguments were not valid JSON: ${call.arguments}`;
653
+ messages.push({ role: 'tool', tool_call_id: call.id, content: invalid });
513
654
  emit({ type: 'tool-result', id: call.id, name: call.name, result: 'invalid JSON arguments', isError: true });
514
655
  continue;
515
656
  }
516
657
  alog('tool', call.name, parsed);
517
- emit({ type: 'tool-call', id: call.id, name: call.name, args: parsed });
658
+ /* TEAM DON'TS (preflight F5): a call the judge will check is announced
659
+ * only once it passes or a person allows it, so its arguments never
660
+ * reach the asker before then. `checkedLevel` is null for every call
661
+ * that is not checked, and always without the port: those emit here,
662
+ * exactly as before. */
663
+ const checkedLevel = dontsChecked.get(call.id) ?? null;
664
+ if (checkedLevel === null)
665
+ emit({ type: 'tool-call', id: call.id, name: call.name, args: parsed });
518
666
  const tool = toolByName.get(call.name);
519
667
  if (!tool) {
520
668
  messages.push({ role: 'tool', tool_call_id: call.id, content: `Error: unknown tool "${call.name}"` });
@@ -595,6 +743,36 @@ export async function runAgent(args) {
595
743
  continue;
596
744
  }
597
745
  }
746
+ /* TEAM DON'TS: after the permission gate allowed it, never before, so a
747
+ * call refused by policy or by a person never reaches the judge. Only a
748
+ * mutating call or one whose level is `owner` (`checkedLevel` above). */
749
+ if (donts && checkedLevel !== null) {
750
+ const mutating = tool.mutates === true;
751
+ emit({ type: 'donts-checking', subject: 'tool', tool: tool.name });
752
+ const held = await donts.checkTool({ tool: tool.name, input: parsed, level: checkedLevel, mutating, recent: recentForJudge(messages) });
753
+ if (held.kind === 'cancelled') {
754
+ dontsCancelled = true;
755
+ break;
756
+ }
757
+ if (held.kind === 'denied') {
758
+ strikes++;
759
+ // The note carries the rule, never the arguments (preflight F4).
760
+ messages.push({ role: 'tool', tool_call_id: call.id, content: blockedToolNote(tool.name, held.rule, held.by ?? 'nobody') });
761
+ // Announced with empty arguments (final fix M1): a live surface pairs
762
+ // the blocked result with a call, while the arguments stay held.
763
+ emit({ type: 'tool-call', id: call.id, name: call.name, args: {} });
764
+ emit({ type: 'tool-result', id: call.id, name: tool.name, result: DONT_TOOL_BLOCKED_RESULT, isError: true });
765
+ if (tool.mutates)
766
+ verifier.denied(call.id, tool.name);
767
+ if (strikes >= DONTS_MAX_STRIKES) {
768
+ dontsStop = true;
769
+ break;
770
+ }
771
+ continue;
772
+ }
773
+ setCallArguments(assistantMsg, call.id, call.arguments);
774
+ emit({ type: 'tool-call', id: call.id, name: call.name, args: parsed });
775
+ }
598
776
  if (tool.mutates)
599
777
  await verifier.before(call.id, tool.name, parsed);
600
778
  // Only when someone is logging file changes, and only for a tool that
@@ -681,6 +859,15 @@ export async function runAgent(args) {
681
859
  emit({ type: 'tool-result', id: call.id, name: call.name, result: errMsg, isError: true, durationMs: Date.now() - startedAt });
682
860
  }
683
861
  }
862
+ if (dontsCancelled) {
863
+ stopReason = 'aborted';
864
+ break;
865
+ }
866
+ if (dontsStop) {
867
+ emit({ type: 'error', message: DONTS_STOPPED });
868
+ stopReason = DONTS_STOP_REASON;
869
+ break;
870
+ }
684
871
  if (policyStop) {
685
872
  stopReason = 'policy';
686
873
  break;
@@ -705,6 +892,19 @@ export async function runAgent(args) {
705
892
  emit({ type: 'done', reason: stopReason });
706
893
  return { messages, text, usage, stopReason, appended: appendedFromTail(), contextTokens, calls };
707
894
  }
895
+ /** Team don'ts: a checked call was committed with '{}' as its arguments; once
896
+ * it is announced its real arguments are put back in the stored message. */
897
+ function setCallArguments(msg, callId, args) {
898
+ if (msg.role !== 'assistant' || !msg.tool_calls)
899
+ return;
900
+ for (const t of msg.tool_calls) {
901
+ if (t.id === callId)
902
+ t.function.arguments = args;
903
+ }
904
+ }
905
+ /** The model-facing result for a checked call whose arguments were not valid
906
+ * JSON: fixed text, so the held arguments are not echoed into the transcript. */
907
+ const DONT_INVALID_JSON_RESULT = 'Error: the tool arguments were not valid JSON.';
708
908
  /** 'deferred' strategy: compact any ALREADY-oversized tool result already
709
909
  * sitting in the incoming history — i.e. produced by an earlier `runAgent()`
710
910
  * call (a prior conversation turn, a resumed session) rather than this one.