clementine-agent 1.18.205 → 1.18.206

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.
@@ -61,7 +61,7 @@ const RESEARCHER_PROMPT = [
61
61
  '',
62
62
  '## Output discipline',
63
63
  '',
64
- 'Return a ONE-PARAGRAPH summary in the format the parent specified. Never raw tool output, never full lists, never unbounded data dumps. If a tool returns 50KB of JSON, extract only the requested fields and discard the rest — your job is to compress.',
64
+ 'Return a ONE-PARAGRAPH summary in the format the parent specified. Hard limit: 250 words unless the parent explicitly asks for a longer artifact. Never raw tool output, never full lists, never unbounded data dumps. If a tool returns 50KB of JSON, extract only the requested fields and discard the rest — your job is to compress.',
65
65
  '',
66
66
  'If you cannot find the requested data, say so in one line. Do not speculate.',
67
67
  ].join('\n');
@@ -106,6 +106,8 @@ const DISCOVERY_PROMPT = [
106
106
  'Recommendation: <which path the orchestrator should fetch next, if any>',
107
107
  '```',
108
108
  '',
109
+ 'Hard output limit: 250 words. Return decision-grade handoff only: paths, counts, sizes, and the next file/action the orchestrator should inspect. Do NOT return a full project report, long markdown excerpts, repeated file inventories, or narrative analysis. If the parent needs detail, point it at the exact file path to read next.',
110
+ '',
109
111
  'If nothing matches, say so in one line.',
110
112
  '',
111
113
  'You are bounded by max 15 turns. Use them wisely — list, scope, summarize, return.',
@@ -31,6 +31,9 @@ export interface RunSummary {
31
31
  failedSideEffects: SideEffectCall[];
32
32
  pendingSideEffects: SideEffectCall[];
33
33
  unknownEffectCalls: SideEffectCall[];
34
+ successfulDelegations: SideEffectCall[];
35
+ failedDelegations: SideEffectCall[];
36
+ pendingDelegations: SideEffectCall[];
34
37
  readOnlyCount: number;
35
38
  errors: Array<{
36
39
  runId: string;
@@ -66,11 +66,23 @@ export function summarizeRunSideEffects(runIds, eventLog = new EventLog()) {
66
66
  const failedSideEffects = [];
67
67
  const pendingSideEffects = [];
68
68
  const unknownEffectCalls = [];
69
+ const successfulDelegations = [];
70
+ const failedDelegations = [];
71
+ const pendingDelegations = [];
69
72
  let readOnlyCount = 0;
70
73
  for (const call of events.filter(isToolCall)) {
71
74
  const verdict = classifyToolCall(call.toolName, asInput(call.toolInput));
72
75
  const result = resultForToolUse(events, call.toolUseId);
73
76
  const item = makeCall(call, result, verdict);
77
+ if (call.toolName === 'Agent') {
78
+ if (!result)
79
+ pendingDelegations.push(item);
80
+ else if (item.result?.successful)
81
+ successfulDelegations.push(item);
82
+ else
83
+ failedDelegations.push(item);
84
+ continue;
85
+ }
74
86
  if (verdict.kind === 'read_only') {
75
87
  readOnlyCount += 1;
76
88
  continue;
@@ -110,6 +122,9 @@ export function summarizeRunSideEffects(runIds, eventLog = new EventLog()) {
110
122
  failedSideEffects,
111
123
  pendingSideEffects,
112
124
  unknownEffectCalls,
125
+ successfulDelegations,
126
+ failedDelegations,
127
+ pendingDelegations,
113
128
  readOnlyCount,
114
129
  errors,
115
130
  ...(lastAssistantText ? { lastAssistantText } : {}),
@@ -120,7 +135,10 @@ export function hasOperationalActivity(summary) {
120
135
  return summary.successfulSideEffects.length > 0
121
136
  || summary.failedSideEffects.length > 0
122
137
  || summary.pendingSideEffects.length > 0
123
- || summary.unknownEffectCalls.length > 0;
138
+ || summary.unknownEffectCalls.length > 0
139
+ || summary.successfulDelegations.length > 0
140
+ || summary.failedDelegations.length > 0
141
+ || summary.pendingDelegations.length > 0;
124
142
  }
125
143
  function toolKindLabel(toolName) {
126
144
  const lower = toolName.toLowerCase();
@@ -206,6 +224,41 @@ function recipientPreview(calls, max = 3) {
206
224
  function formatGroupedLines(prefix, calls) {
207
225
  return groupCounts(calls).map((group) => `- ${group.count} ${group.label} ${prefix}${recipientPreview(group.calls)}`);
208
226
  }
227
+ function collectResultText(value) {
228
+ if (typeof value === 'string')
229
+ return value;
230
+ if (Array.isArray(value))
231
+ return value.map(collectResultText).filter(Boolean).join('\n');
232
+ if (!value || typeof value !== 'object')
233
+ return '';
234
+ const obj = value;
235
+ return ['text', 'content', 'result', 'message']
236
+ .map((key) => collectResultText(obj[key]))
237
+ .filter(Boolean)
238
+ .join('\n');
239
+ }
240
+ function extractAgentArchivePath(text) {
241
+ return text.match(/Full payload archived at `([^`]+)`/)?.[1]
242
+ ?? text.match(/Full output:\s*([^\n]+)/)?.[1]?.trim();
243
+ }
244
+ function extractAgentId(text) {
245
+ return text.match(/\bagentId:\s*([a-zA-Z0-9_-]+)/)?.[1];
246
+ }
247
+ function formatDelegationCall(call, status) {
248
+ const description = firstString(call.input.description, call.input.task, call.input.prompt)?.slice(0, 120);
249
+ const subagentType = firstString(call.input.subagent_type, call.input.subagentType);
250
+ const resultText = call.result ? collectResultText(call.result.raw) : '';
251
+ const agentId = extractAgentId(resultText);
252
+ const archivePath = extractAgentArchivePath(resultText);
253
+ const pieces = [
254
+ subagentType ? `${subagentType} subagent` : 'subagent',
255
+ status,
256
+ description ? `for "${description}"` : undefined,
257
+ agentId ? `agentId ${agentId}` : undefined,
258
+ archivePath ? `archive ${archivePath}` : undefined,
259
+ ].filter(Boolean);
260
+ return `- ${pieces.join(' · ')}`;
261
+ }
209
262
  export function formatOverflowRecoveryMessage(summary) {
210
263
  const lines = [
211
264
  'That run hit the context limit after some work had already happened.',
@@ -216,12 +269,28 @@ export function formatOverflowRecoveryMessage(summary) {
216
269
  lines.push(...formatGroupedLines('completed', summary.successfulSideEffects));
217
270
  lines.push('');
218
271
  }
219
- if (summary.failedSideEffects.length > 0 || summary.pendingSideEffects.length > 0 || summary.unknownEffectCalls.length > 0) {
272
+ if (summary.successfulDelegations.length > 0) {
273
+ lines.push('Delegated work completed before overflow:');
274
+ for (const call of summary.successfulDelegations.slice(0, 5))
275
+ lines.push(formatDelegationCall(call, 'completed'));
276
+ if (summary.successfulDelegations.length > 5)
277
+ lines.push(`- ...and ${summary.successfulDelegations.length - 5} more completed subagent calls`);
278
+ lines.push('');
279
+ }
280
+ if (summary.failedSideEffects.length > 0
281
+ || summary.pendingSideEffects.length > 0
282
+ || summary.unknownEffectCalls.length > 0
283
+ || summary.failedDelegations.length > 0
284
+ || summary.pendingDelegations.length > 0) {
220
285
  lines.push('Needs attention:');
221
286
  if (summary.failedSideEffects.length > 0)
222
287
  lines.push(...formatGroupedLines('failed', summary.failedSideEffects));
223
288
  if (summary.pendingSideEffects.length > 0)
224
289
  lines.push(...formatGroupedLines('started, no confirmation', summary.pendingSideEffects));
290
+ for (const call of summary.failedDelegations.slice(0, 5))
291
+ lines.push(formatDelegationCall(call, 'failed'));
292
+ for (const call of summary.pendingDelegations.slice(0, 5))
293
+ lines.push(formatDelegationCall(call, 'started, no confirmation'));
225
294
  if (summary.unknownEffectCalls.length > 0)
226
295
  lines.push(`- ${summary.unknownEffectCalls.length} tool call(s) had unknown external effect`);
227
296
  lines.push('');
@@ -261,6 +330,12 @@ export function buildContinuationPrompt(summary, originalRequest) {
261
330
  lines.push(`- ...and ${summary.successfulSideEffects.length - 80} more completed side effects in the event log.`);
262
331
  lines.push('');
263
332
  }
333
+ if (summary.successfulDelegations.length > 0) {
334
+ lines.push('Completed delegated work. Do not repeat discovery/research already done unless the archive is insufficient:');
335
+ for (const call of summary.successfulDelegations.slice(0, 20))
336
+ lines.push(formatDelegationCall(call, 'completed'));
337
+ lines.push('');
338
+ }
264
339
  if (summary.failedSideEffects.length > 0) {
265
340
  lines.push('Failed side effects that may need retry or reconciliation:');
266
341
  for (const call of summary.failedSideEffects.slice(0, 30))
@@ -80,6 +80,8 @@ export interface GuardRunStats {
80
80
  export declare function estimateBytes(value: unknown): number;
81
81
  interface CompressionContext {
82
82
  toolName: string;
83
+ toolUseId?: string;
84
+ toolInput?: unknown;
83
85
  archivePath: string | null;
84
86
  cap: number;
85
87
  }
@@ -117,6 +117,101 @@ function tryListShrink(value, ctx) {
117
117
  }
118
118
  return null;
119
119
  }
120
+ function collectTextFragments(value) {
121
+ if (typeof value === 'string')
122
+ return [value];
123
+ if (Array.isArray(value))
124
+ return value.flatMap((item) => collectTextFragments(item));
125
+ if (!value || typeof value !== 'object')
126
+ return [];
127
+ const obj = value;
128
+ const out = [];
129
+ for (const key of ['text', 'content', 'result', 'message']) {
130
+ const v = obj[key];
131
+ if (typeof v === 'string')
132
+ out.push(v);
133
+ else if (Array.isArray(v) || (v && typeof v === 'object'))
134
+ out.push(...collectTextFragments(v));
135
+ }
136
+ return out;
137
+ }
138
+ function objectField(value, key) {
139
+ return value && typeof value === 'object' && !Array.isArray(value)
140
+ ? typeof value[key] === 'string'
141
+ ? String(value[key]).trim()
142
+ : undefined
143
+ : undefined;
144
+ }
145
+ function extractAgentId(text) {
146
+ return text.match(/\bagentId:\s*([a-zA-Z0-9_-]+)/)?.[1];
147
+ }
148
+ function extractUsageLine(text) {
149
+ const match = text.match(/<usage>[\s\S]*?(?:<\/usage>|$)/);
150
+ return match?.[0]?.replace(/\s+/g, ' ').trim().slice(0, 220);
151
+ }
152
+ function stripAgentBoilerplate(text) {
153
+ return text
154
+ .replace(/agentId:\s*[a-zA-Z0-9_-]+[\s\S]*$/i, '')
155
+ .replace(/<usage>[\s\S]*$/i, '')
156
+ .replace(/^\s*(perfect|great|okay|ok)[.!]?\s+now\s+i\s+have[^\n]*\n+/i, '')
157
+ .trim();
158
+ }
159
+ function compactMarkdownLines(text) {
160
+ const lines = stripAgentBoilerplate(text)
161
+ .split(/\r?\n/)
162
+ .map((line) => line.trim())
163
+ .filter((line) => line && line !== '---' && line !== '```');
164
+ const keep = [];
165
+ for (const line of lines) {
166
+ if (keep.length >= 26)
167
+ break;
168
+ if (/^#{1,4}\s/.test(line) || /^[-*]\s/.test(line) || /^\d+\.\s/.test(line) || /^[A-Z][^:]{2,60}:/.test(line)) {
169
+ keep.push(line);
170
+ continue;
171
+ }
172
+ if (keep.length < 8 && line.length <= 220)
173
+ keep.push(line);
174
+ }
175
+ return keep.join('\n');
176
+ }
177
+ function fitUnderBytes(text, maxBytes) {
178
+ if (estimateBytes(text) <= maxBytes)
179
+ return text;
180
+ const marker = '\n\n[...compact handoff truncated; read the archived Agent result for full detail.]';
181
+ let head = text.slice(0, Math.max(200, maxBytes - estimateBytes(marker) - 200));
182
+ while (head.length > 200 && estimateBytes(head + marker) > maxBytes) {
183
+ head = head.slice(0, Math.floor(head.length * 0.9));
184
+ }
185
+ return `${head.trimEnd()}${marker}`;
186
+ }
187
+ function tryAgentShrink(value, ctx) {
188
+ if (ctx.toolName !== 'Agent')
189
+ return null;
190
+ const fragments = collectTextFragments(value);
191
+ const text = fragments.join('\n\n').trim();
192
+ if (!text)
193
+ return null;
194
+ const subagentType = objectField(ctx.toolInput, 'subagent_type');
195
+ const description = objectField(ctx.toolInput, 'description');
196
+ const agentId = extractAgentId(text);
197
+ const usage = extractUsageLine(text);
198
+ const summary = compactMarkdownLines(text);
199
+ const archive = archiveHint(ctx, 'full Agent result');
200
+ const lines = [
201
+ '[Clementine compacted this Agent result to protect the parent chat context.]',
202
+ subagentType ? `Subagent: ${subagentType}` : undefined,
203
+ description ? `Task: ${description}` : undefined,
204
+ agentId ? `agentId: ${agentId}` : undefined,
205
+ usage,
206
+ archive,
207
+ '',
208
+ 'Decision-grade handoff:',
209
+ summary || fitUnderBytes(stripAgentBoilerplate(text), Math.max(1_000, Math.floor(ctx.cap * 0.6))),
210
+ '',
211
+ 'Use this handoff to continue. Read the archived result only if the missing detail is necessary.',
212
+ ].filter((line) => typeof line === 'string' && line.length > 0);
213
+ return fitUnderBytes(lines.join('\n'), ctx.cap);
214
+ }
120
215
  function shrinkArray(arr, ctx) {
121
216
  if (arr.length <= 6) {
122
217
  // Don't trim short lists; the bloat is somewhere else (likely a fat body).
@@ -244,6 +339,19 @@ export function compressToolOutput(_toolName, rawOutput, ctx) {
244
339
  if (originalBytes <= ctx.cap) {
245
340
  return { output: rawOutput, bytesShed: 0, ceilingHit: false, passthrough: true };
246
341
  }
342
+ // Agent tool results are subagent handoffs to the parent orchestrator.
343
+ // Preserve the decision-grade summary and archive the full result instead
344
+ // of letting a verbose report refill the parent context after compaction.
345
+ const agentShrunk = tryAgentShrink(rawOutput, ctx);
346
+ if (agentShrunk !== null) {
347
+ const bytes = estimateBytes(agentShrunk);
348
+ return {
349
+ output: agentShrunk,
350
+ bytesShed: Math.max(0, originalBytes - bytes),
351
+ ceilingHit: originalBytes > ctx.cap * 2,
352
+ passthrough: false,
353
+ };
354
+ }
247
355
  // Pass 1: list-shape shrink (preserves structure).
248
356
  const shrunk1 = tryListShrink(rawOutput, ctx);
249
357
  if (shrunk1 !== null) {
@@ -294,7 +402,7 @@ export function buildGuardHooks(opts) {
294
402
  const toolUseId = String(toolUseID ?? evt.tool_use_id ?? 'unknown');
295
403
  const rawOutput = evt.tool_response;
296
404
  stats.inspected += 1;
297
- const usageRatio = opts.usageRatio ? safeRatio(opts.usageRatio) : 0;
405
+ const usageRatio = Math.max(opts.usageRatio ? safeRatio(opts.usageRatio) : 0, stats.compactions > 0 ? 0.75 : 0);
298
406
  const { softCap } = resolveCap(toolName, config, usageRatio);
299
407
  const originalBytes = estimateBytes(rawOutput);
300
408
  if (originalBytes <= softCap) {
@@ -307,6 +415,7 @@ export function buildGuardHooks(opts) {
307
415
  const archivePath = archivePayload(opts.archiveBaseDir ?? BASE_DIR, opts.runId, toolUseId, toolName, rawOutput);
308
416
  const outcome = compressToolOutput(toolName, rawOutput, {
309
417
  toolName,
418
+ toolInput: evt.tool_input,
310
419
  toolUseId,
311
420
  archivePath,
312
421
  cap: softCap,
package/dist/config.js CHANGED
@@ -552,7 +552,10 @@ export const TOOL_OUTPUT_GUARD = {
552
552
  softLimitBytes: getEnvOrJsonNumber('TOOL_OUTPUT_GUARD_SOFT_BYTES', json.toolOutputGuard?.softLimitBytes, 30_000),
553
553
  hardLimitBytes: getEnvOrJsonNumber('TOOL_OUTPUT_GUARD_HARD_BYTES', json.toolOutputGuard?.hardLimitBytes, 200_000),
554
554
  adaptive: boolEnv('TOOL_OUTPUT_GUARD_ADAPTIVE', json.toolOutputGuard?.adaptive, true),
555
- perTool: { ...(json.toolOutputGuard?.perTool ?? {}) },
555
+ // Agent results are especially dangerous: even a "medium" subagent report
556
+ // refills the parent orchestrator after compaction. Keep the handoff tight;
557
+ // the full result is archived by tool-output-guard.
558
+ perTool: { Agent: 4_000, ...(json.toolOutputGuard?.perTool ?? {}) },
556
559
  };
557
560
  export const DEFAULT_MODEL_TIER = (getEnvOrJson('DEFAULT_MODEL_TIER', json.models?.default, 'sonnet'));
558
561
  export const MODEL = MODELS[DEFAULT_MODEL_TIER] ?? MODELS.sonnet;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.205",
3
+ "version": "1.18.206",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",