@agentconnect/host 0.2.5 → 0.2.6

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/host.js CHANGED
@@ -9,7 +9,7 @@ import { listModels, listRecentModels, providers, resolveProviderForModel, } fro
9
9
  import { debugLog, setSpawnLogging } from './providers/utils.js';
10
10
  import { createObservedTracker } from './observed.js';
11
11
  import { createStorage } from './storage.js';
12
- import { buildSummaryPrompt, getSummaryModel, runSummaryPrompt, } from './summary.js';
12
+ import { buildSummaryPrompt, buildSummaryPromptWithOverride, getSummaryModel, runSummaryPrompt, } from './summary.js';
13
13
  function send(socket, payload) {
14
14
  socket.send(JSON.stringify(payload));
15
15
  }
@@ -126,6 +126,29 @@ function createHostRuntime(options) {
126
126
  return null;
127
127
  }
128
128
  }
129
+ function normalizeSummaryMode(value) {
130
+ const raw = typeof value === 'string' ? value.trim().toLowerCase() : '';
131
+ if (raw === 'auto' || raw === 'off' || raw === 'force')
132
+ return raw;
133
+ return undefined;
134
+ }
135
+ function normalizeSummaryPrompt(value) {
136
+ if (typeof value !== 'string')
137
+ return undefined;
138
+ const trimmed = value.trim();
139
+ return trimmed ? trimmed : undefined;
140
+ }
141
+ function resolveSummaryMode(session, requested) {
142
+ if (requested)
143
+ return requested;
144
+ return session.summaryMode ?? 'auto';
145
+ }
146
+ function resolveEffectiveSummaryMode(session, requested) {
147
+ const mode = resolveSummaryMode(session, requested);
148
+ if (mode === 'auto' && session.summaryAutoUsed)
149
+ return 'off';
150
+ return mode;
151
+ }
129
152
  function recordCapability(capability) {
130
153
  observedTracker.record(capability);
131
154
  }
@@ -183,17 +206,22 @@ function createHostRuntime(options) {
183
206
  function clearSummarySeed(session) {
184
207
  session.summarySeed = undefined;
185
208
  session.summaryReasoning = undefined;
209
+ session.summaryNextMode = undefined;
210
+ session.summaryNextPrompt = undefined;
186
211
  }
187
212
  async function startPromptSummary(options) {
188
- const { sessionId, session, message, reasoning, emit } = options;
213
+ const { sessionId, session, message, reasoning, summaryPrompt, mode, emit } = options;
189
214
  if (session.summaryRequested)
190
215
  return;
191
216
  session.summaryRequested = true;
217
+ let completed = false;
192
218
  try {
193
219
  const provider = providers[session.providerId];
194
220
  if (!provider)
195
221
  return;
196
- const prompt = buildSummaryPrompt(message, reasoning);
222
+ const prompt = summaryPrompt
223
+ ? buildSummaryPromptWithOverride(summaryPrompt, message, reasoning)
224
+ : buildSummaryPrompt(message, reasoning);
197
225
  const summaryModel = getSummaryModel(session.providerId);
198
226
  const cwd = session.cwd || basePath;
199
227
  const repoRoot = session.repoRoot || basePath;
@@ -213,9 +241,13 @@ function createHostRuntime(options) {
213
241
  model: result.model ?? null,
214
242
  createdAt: new Date().toISOString(),
215
243
  }, session);
244
+ completed = true;
216
245
  }
217
246
  finally {
218
247
  session.summaryRequested = false;
248
+ if (!completed && mode === 'auto') {
249
+ session.summaryAutoUsed = true;
250
+ }
219
251
  if (session.summarySeed) {
220
252
  maybeStartPromptSummary({
221
253
  sessionId,
@@ -342,6 +374,9 @@ function createHostRuntime(options) {
342
374
  }
343
375
  function maybeStartPromptSummary(options) {
344
376
  const { sessionId, session, emit, trigger } = options;
377
+ const mode = resolveEffectiveSummaryMode(session, session.summaryNextMode);
378
+ if (mode === 'off')
379
+ return;
345
380
  if (session.summaryRequested)
346
381
  return;
347
382
  if (!session.summarySeed)
@@ -350,8 +385,17 @@ function createHostRuntime(options) {
350
385
  return;
351
386
  const message = session.summarySeed;
352
387
  const reasoning = session.summaryReasoning;
388
+ const summaryPrompt = session.summaryNextPrompt ?? session.summaryPrompt;
353
389
  clearSummarySeed(session);
354
- void startPromptSummary({ sessionId, session, message, reasoning, emit });
390
+ void startPromptSummary({
391
+ sessionId,
392
+ session,
393
+ message,
394
+ reasoning,
395
+ summaryPrompt,
396
+ mode,
397
+ emit,
398
+ });
355
399
  }
356
400
  function sessionSummaryKey(sessionId) {
357
401
  return `session:${sessionId}:summary`;
@@ -360,9 +404,6 @@ function createHostRuntime(options) {
360
404
  if (!payload.summary)
361
405
  return;
362
406
  if (session) {
363
- if (session.summarySource === 'claude-log' && payload.source === 'prompt') {
364
- return;
365
- }
366
407
  if (session.summary === payload.summary && session.summarySource === payload.source) {
367
408
  return;
368
409
  }
@@ -370,6 +411,7 @@ function createHostRuntime(options) {
370
411
  session.summarySource = payload.source;
371
412
  session.summaryModel = payload.model ?? null;
372
413
  session.summaryCreatedAt = payload.createdAt;
414
+ session.summaryAutoUsed = true;
373
415
  }
374
416
  emitSessionEvent(emit, sessionId, 'summary', payload);
375
417
  storage.set(sessionSummaryKey(sessionId), payload);
@@ -602,6 +644,8 @@ function createHostRuntime(options) {
602
644
  else {
603
645
  recordProviderCapability(providerId);
604
646
  }
647
+ const summaryMode = normalizeSummaryMode(params.summary?.mode);
648
+ const summaryPrompt = normalizeSummaryPrompt(params.summary?.prompt);
605
649
  sessions.set(sessionId, {
606
650
  id: sessionId,
607
651
  providerId,
@@ -614,6 +658,9 @@ function createHostRuntime(options) {
614
658
  providerDetailLevel: providerDetailLevel === 'raw' || providerDetailLevel === 'minimal'
615
659
  ? providerDetailLevel
616
660
  : undefined,
661
+ summaryMode: summaryMode === 'force' ? 'auto' : summaryMode ?? 'auto',
662
+ summaryPrompt,
663
+ summaryAutoUsed: false,
617
664
  });
618
665
  responder.reply(id, { sessionId });
619
666
  return;
@@ -643,6 +690,16 @@ function createHostRuntime(options) {
643
690
  existing.providerDetailLevel = level;
644
691
  }
645
692
  }
693
+ if ('summary' in params) {
694
+ const summaryMode = normalizeSummaryMode(params.summary?.mode);
695
+ const summaryPrompt = normalizeSummaryPrompt(params.summary?.prompt);
696
+ if (summaryMode) {
697
+ existing.summaryMode = summaryMode === 'force' ? 'auto' : summaryMode;
698
+ }
699
+ if (summaryPrompt !== undefined) {
700
+ existing.summaryPrompt = summaryPrompt;
701
+ }
702
+ }
646
703
  if (!existing.model && typeof params.model === 'string' && params.model.trim()) {
647
704
  const candidate = params.model.trim();
648
705
  const matches = await isModelForProvider(candidate, existing.providerId);
@@ -698,7 +755,13 @@ function createHostRuntime(options) {
698
755
  return;
699
756
  }
700
757
  }
701
- if (message.trim()) {
758
+ const summaryMode = resolveEffectiveSummaryMode(session, normalizeSummaryMode(params.summary?.mode));
759
+ const summaryPrompt = normalizeSummaryPrompt(params.summary?.prompt);
760
+ session.summaryNextMode = summaryMode;
761
+ if (summaryPrompt !== undefined) {
762
+ session.summaryNextPrompt = summaryPrompt;
763
+ }
764
+ if (summaryMode !== 'off' && message.trim()) {
702
765
  session.summarySeed = message;
703
766
  session.summaryReasoning = '';
704
767
  }
@@ -903,6 +903,180 @@ function mapClaudeModel(model) {
903
903
  return value.replace('claude-', '');
904
904
  return model;
905
905
  }
906
+ function extractUsageFromValue(value) {
907
+ if (!value || typeof value !== 'object')
908
+ return null;
909
+ const usage = value;
910
+ const toNumber = (v) => {
911
+ if (typeof v === 'number' && Number.isFinite(v))
912
+ return v;
913
+ if (typeof v === 'string' && v.trim()) {
914
+ const parsed = Number(v);
915
+ return Number.isFinite(parsed) ? parsed : undefined;
916
+ }
917
+ return undefined;
918
+ };
919
+ const input = toNumber(usage.input_tokens ?? usage.prompt_tokens ?? usage.inputTokens ?? usage.promptTokens);
920
+ const output = toNumber(usage.output_tokens ?? usage.completion_tokens ?? usage.outputTokens ?? usage.completionTokens);
921
+ const total = toNumber(usage.total_tokens ?? usage.totalTokens);
922
+ let cached = toNumber(usage.cached_input_tokens ?? usage.cachedInputTokens);
923
+ const cacheCreation = toNumber(usage.cache_creation_input_tokens ?? usage.cacheCreationInputTokens);
924
+ const cacheRead = toNumber(usage.cache_read_input_tokens ?? usage.cacheReadInputTokens);
925
+ if (cached === undefined) {
926
+ const sum = (cacheCreation ?? 0) + (cacheRead ?? 0);
927
+ if (sum > 0)
928
+ cached = sum;
929
+ }
930
+ const reasoning = toNumber(usage.reasoning_tokens ?? usage.reasoningTokens);
931
+ const out = {};
932
+ if (input !== undefined)
933
+ out.input_tokens = input;
934
+ if (output !== undefined)
935
+ out.output_tokens = output;
936
+ if (total !== undefined)
937
+ out.total_tokens = total;
938
+ if (cached !== undefined)
939
+ out.cached_input_tokens = cached;
940
+ if (reasoning !== undefined)
941
+ out.reasoning_tokens = reasoning;
942
+ return Object.keys(out).length ? out : null;
943
+ }
944
+ function pickModelUsage(value) {
945
+ if (!value || typeof value !== 'object')
946
+ return null;
947
+ const entries = Object.values(value);
948
+ for (const entry of entries) {
949
+ if (entry && typeof entry === 'object')
950
+ return entry;
951
+ }
952
+ return null;
953
+ }
954
+ function extractClaudeUsage(msg) {
955
+ const usage = msg.usage ??
956
+ msg.message?.usage ??
957
+ msg.event?.usage ??
958
+ msg.delta?.usage ??
959
+ msg.token_usage ??
960
+ msg.tokenUsage;
961
+ if (usage)
962
+ return extractUsageFromValue(usage);
963
+ const resultRecord = msg.result && typeof msg.result === 'object' ? msg.result : null;
964
+ const nestedUsage = resultRecord?.usage ??
965
+ resultRecord?.token_usage ??
966
+ resultRecord?.tokenUsage ??
967
+ resultRecord?.token_usage;
968
+ if (nestedUsage)
969
+ return extractUsageFromValue(nestedUsage);
970
+ const modelUsage = pickModelUsage(msg.modelUsage ?? msg.message?.modelUsage ?? resultRecord?.modelUsage);
971
+ return extractUsageFromValue(modelUsage);
972
+ }
973
+ function extractClaudeContextUsage(msg, usage) {
974
+ const context = msg.context_usage ??
975
+ msg.contextUsage ??
976
+ msg.message?.context_usage ??
977
+ msg.message?.contextUsage ??
978
+ msg.event?.context_usage ??
979
+ msg.event?.contextUsage;
980
+ const resultRecord = msg.result && typeof msg.result === 'object' ? msg.result : null;
981
+ const modelUsage = pickModelUsage(msg.modelUsage ?? msg.message?.modelUsage ?? resultRecord?.modelUsage);
982
+ const toNumber = (v) => {
983
+ if (typeof v === 'number' && Number.isFinite(v))
984
+ return v;
985
+ if (typeof v === 'string' && v.trim()) {
986
+ const parsed = Number(v);
987
+ return Number.isFinite(parsed) ? parsed : undefined;
988
+ }
989
+ return undefined;
990
+ };
991
+ const toBoolean = (v) => typeof v === 'boolean' ? v : undefined;
992
+ const contextWindow = toNumber(context?.context_window ??
993
+ context?.contextWindow ??
994
+ modelUsage?.contextWindow ??
995
+ modelUsage?.context_window ??
996
+ msg.context_window ??
997
+ msg.contextWindow);
998
+ let contextTokens = toNumber(context?.context_tokens ??
999
+ context?.contextTokens ??
1000
+ msg.context_tokens ??
1001
+ msg.contextTokens);
1002
+ let contextCachedTokens = toNumber(context?.context_cached_tokens ??
1003
+ context?.contextCachedTokens ??
1004
+ msg.context_cached_tokens ??
1005
+ msg.contextCachedTokens);
1006
+ let contextRemainingTokens = toNumber(context?.context_remaining_tokens ??
1007
+ context?.contextRemainingTokens ??
1008
+ msg.context_remaining_tokens ??
1009
+ msg.contextRemainingTokens);
1010
+ const contextTruncated = toBoolean(context?.context_truncated ??
1011
+ context?.contextTruncated ??
1012
+ msg.context_truncated ??
1013
+ msg.contextTruncated);
1014
+ if (contextCachedTokens === undefined) {
1015
+ const cached = toNumber(context?.cache_creation_input_tokens) ??
1016
+ toNumber(context?.cacheCreationInputTokens) ??
1017
+ toNumber(context?.cache_read_input_tokens) ??
1018
+ toNumber(context?.cacheReadInputTokens);
1019
+ if (cached !== undefined)
1020
+ contextCachedTokens = cached;
1021
+ }
1022
+ if (contextTokens === undefined) {
1023
+ if (usage?.input_tokens !== undefined &&
1024
+ usage?.cached_input_tokens !== undefined) {
1025
+ contextTokens = usage.input_tokens + usage.cached_input_tokens;
1026
+ }
1027
+ else if (usage?.input_tokens !== undefined) {
1028
+ contextTokens = usage.input_tokens;
1029
+ }
1030
+ }
1031
+ if (contextCachedTokens === undefined && usage?.cached_input_tokens !== undefined) {
1032
+ contextCachedTokens = usage.cached_input_tokens;
1033
+ }
1034
+ if (contextRemainingTokens === undefined &&
1035
+ contextWindow !== undefined &&
1036
+ contextTokens !== undefined) {
1037
+ contextRemainingTokens = Math.max(0, contextWindow - contextTokens);
1038
+ }
1039
+ const out = {};
1040
+ if (contextWindow !== undefined)
1041
+ out.context_window = contextWindow;
1042
+ if (contextTokens !== undefined)
1043
+ out.context_tokens = contextTokens;
1044
+ if (contextCachedTokens !== undefined)
1045
+ out.context_cached_tokens = contextCachedTokens;
1046
+ if (contextRemainingTokens !== undefined)
1047
+ out.context_remaining_tokens = contextRemainingTokens;
1048
+ if (contextTruncated !== undefined)
1049
+ out.context_truncated = contextTruncated;
1050
+ return Object.keys(out).length ? out : null;
1051
+ }
1052
+ function mergeUsage(current, next) {
1053
+ const out = { ...(current ?? {}) };
1054
+ if (next.input_tokens !== undefined)
1055
+ out.input_tokens = next.input_tokens;
1056
+ if (next.output_tokens !== undefined)
1057
+ out.output_tokens = next.output_tokens;
1058
+ if (next.total_tokens !== undefined)
1059
+ out.total_tokens = next.total_tokens;
1060
+ if (next.cached_input_tokens !== undefined)
1061
+ out.cached_input_tokens = next.cached_input_tokens;
1062
+ if (next.reasoning_tokens !== undefined)
1063
+ out.reasoning_tokens = next.reasoning_tokens;
1064
+ return out;
1065
+ }
1066
+ function mergeContextUsage(current, next) {
1067
+ const out = { ...(current ?? {}) };
1068
+ if (next.context_window !== undefined)
1069
+ out.context_window = next.context_window;
1070
+ if (next.context_tokens !== undefined)
1071
+ out.context_tokens = next.context_tokens;
1072
+ if (next.context_cached_tokens !== undefined)
1073
+ out.context_cached_tokens = next.context_cached_tokens;
1074
+ if (next.context_remaining_tokens !== undefined)
1075
+ out.context_remaining_tokens = next.context_remaining_tokens;
1076
+ if (next.context_truncated !== undefined)
1077
+ out.context_truncated = next.context_truncated;
1078
+ return out;
1079
+ }
906
1080
  function extractSessionId(msg) {
907
1081
  const direct = msg.session_id ?? msg.sessionId;
908
1082
  if (typeof direct === 'string' && direct)
@@ -1004,6 +1178,11 @@ export function runClaudePrompt({ prompt, system, resumeSessionId, model, cwd, p
1004
1178
  let finalSessionId = null;
1005
1179
  let didFinalize = false;
1006
1180
  let sawError = false;
1181
+ let usageEmitted = false;
1182
+ let latestUsage = null;
1183
+ let latestContextUsage = null;
1184
+ let latestUsageDetail;
1185
+ let latestContextUsageDetail;
1007
1186
  const toolBlocks = new Map();
1008
1187
  const thinkingBlocks = new Set();
1009
1188
  const includeRaw = providerDetailLevel === 'raw';
@@ -1027,11 +1206,46 @@ export function runClaudePrompt({ prompt, system, resumeSessionId, model, cwd, p
1027
1206
  if (sawError)
1028
1207
  return;
1029
1208
  sawError = true;
1209
+ emitUsageIfAvailable();
1030
1210
  emit({ type: 'error', message });
1031
1211
  };
1032
1212
  const emitFinal = (text, providerDetail) => {
1213
+ emitUsageIfAvailable();
1033
1214
  emit({ type: 'final', text, providerDetail });
1034
1215
  };
1216
+ const captureUsage = (msg, providerDetail) => {
1217
+ const usage = extractClaudeUsage(msg);
1218
+ if (usage) {
1219
+ latestUsage = mergeUsage(latestUsage, usage);
1220
+ latestUsageDetail = providerDetail;
1221
+ }
1222
+ const contextUsage = extractClaudeContextUsage(msg, latestUsage);
1223
+ if (contextUsage) {
1224
+ latestContextUsage = mergeContextUsage(latestContextUsage, contextUsage);
1225
+ latestContextUsageDetail = providerDetail;
1226
+ }
1227
+ };
1228
+ const emitUsageIfAvailable = () => {
1229
+ if (usageEmitted)
1230
+ return;
1231
+ if (!latestUsage && !latestContextUsage)
1232
+ return;
1233
+ if (latestUsage) {
1234
+ emit({
1235
+ type: 'usage',
1236
+ usage: latestUsage,
1237
+ providerDetail: latestUsageDetail,
1238
+ });
1239
+ }
1240
+ if (latestContextUsage) {
1241
+ emit({
1242
+ type: 'context_usage',
1243
+ contextUsage: latestContextUsage,
1244
+ providerDetail: latestContextUsageDetail ?? latestUsageDetail,
1245
+ });
1246
+ }
1247
+ usageEmitted = true;
1248
+ };
1035
1249
  const handleLine = (line) => {
1036
1250
  const parsed = safeJsonParse(line);
1037
1251
  if (!parsed || typeof parsed !== 'object') {
@@ -1046,6 +1260,8 @@ export function runClaudePrompt({ prompt, system, resumeSessionId, model, cwd, p
1046
1260
  finalSessionId = sid;
1047
1261
  const msgType = String(msg.type ?? '');
1048
1262
  let handled = false;
1263
+ const detail = buildProviderDetail(msgType || 'unknown', {}, msg);
1264
+ captureUsage(msg, detail);
1049
1265
  if (msgType === 'assistant' || msgType === 'user' || msgType === 'system') {
1050
1266
  const role = msg.message?.role === 'assistant' ||
1051
1267
  msg.message?.role === 'user' ||
@@ -1060,7 +1276,7 @@ export function runClaudePrompt({ prompt, system, resumeSessionId, model, cwd, p
1060
1276
  role,
1061
1277
  content,
1062
1278
  contentParts: rawContent ?? null,
1063
- providerDetail: buildProviderDetail(msgType, {}, msg),
1279
+ providerDetail: detail,
1064
1280
  });
1065
1281
  handled = true;
1066
1282
  }
@@ -1152,7 +1368,7 @@ export function runClaudePrompt({ prompt, system, resumeSessionId, model, cwd, p
1152
1368
  emit({
1153
1369
  type: 'delta',
1154
1370
  text: delta,
1155
- providerDetail: buildProviderDetail(msgType || 'delta', {}, msg),
1371
+ providerDetail: detail,
1156
1372
  });
1157
1373
  return;
1158
1374
  }
@@ -1162,21 +1378,21 @@ export function runClaudePrompt({ prompt, system, resumeSessionId, model, cwd, p
1162
1378
  emit({
1163
1379
  type: 'delta',
1164
1380
  text: assistant,
1165
- providerDetail: buildProviderDetail(msgType || 'assistant', {}, msg),
1381
+ providerDetail: detail,
1166
1382
  });
1167
1383
  return;
1168
1384
  }
1169
1385
  const result = extractResultText(msg);
1170
1386
  if (result && !didFinalize && !sawError) {
1171
1387
  didFinalize = true;
1172
- emitFinal(aggregated || result, buildProviderDetail('result', {}, msg));
1388
+ emitFinal(aggregated || result, detail);
1173
1389
  handled = true;
1174
1390
  }
1175
1391
  if (!handled) {
1176
1392
  emit({
1177
1393
  type: 'detail',
1178
1394
  provider: 'claude',
1179
- providerDetail: buildProviderDetail(msgType || 'unknown', {}, msg),
1395
+ providerDetail: detail,
1180
1396
  });
1181
1397
  }
1182
1398
  };
@@ -1193,6 +1409,9 @@ export function runClaudePrompt({ prompt, system, resumeSessionId, model, cwd, p
1193
1409
  emitFinal(aggregated);
1194
1410
  }
1195
1411
  }
1412
+ if (!usageEmitted) {
1413
+ emitUsageIfAvailable();
1414
+ }
1196
1415
  resolve({ sessionId: finalSessionId });
1197
1416
  });
1198
1417
  child.on('error', (err) => {
@@ -430,11 +430,20 @@ function extractUsage(ev) {
430
430
  const usage = ev.usage ?? ev.token_usage ?? ev.tokens ?? ev.tokenUsage;
431
431
  if (!usage || typeof usage !== 'object')
432
432
  return null;
433
- const toNumber = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
433
+ const toNumber = (v) => {
434
+ if (typeof v === 'number' && Number.isFinite(v))
435
+ return v;
436
+ if (typeof v === 'string' && v.trim()) {
437
+ const parsed = Number(v);
438
+ return Number.isFinite(parsed) ? parsed : undefined;
439
+ }
440
+ return undefined;
441
+ };
434
442
  const input = toNumber(usage.input_tokens ?? usage.prompt_tokens ?? usage.inputTokens ?? usage.promptTokens);
435
443
  const output = toNumber(usage.output_tokens ?? usage.completion_tokens ?? usage.outputTokens ?? usage.completionTokens);
436
444
  const total = toNumber(usage.total_tokens ?? usage.totalTokens);
437
445
  const cached = toNumber(usage.cached_input_tokens ?? usage.cachedInputTokens);
446
+ const reasoning = toNumber(usage.reasoning_tokens ?? usage.reasoningTokens);
438
447
  const out = {};
439
448
  if (input !== undefined)
440
449
  out.input_tokens = input;
@@ -444,6 +453,61 @@ function extractUsage(ev) {
444
453
  out.total_tokens = total;
445
454
  if (cached !== undefined)
446
455
  out.cached_input_tokens = cached;
456
+ if (reasoning !== undefined)
457
+ out.reasoning_tokens = reasoning;
458
+ return Object.keys(out).length ? out : null;
459
+ }
460
+ function extractContextUsage(ev, usage) {
461
+ const context = ev.context_usage ?? ev.contextUsage;
462
+ const toNumber = (v) => {
463
+ if (typeof v === 'number' && Number.isFinite(v))
464
+ return v;
465
+ if (typeof v === 'string' && v.trim()) {
466
+ const parsed = Number(v);
467
+ return Number.isFinite(parsed) ? parsed : undefined;
468
+ }
469
+ return undefined;
470
+ };
471
+ const toBoolean = (v) => typeof v === 'boolean' ? v : undefined;
472
+ const contextWindow = toNumber(context?.context_window ?? context?.contextWindow ?? ev.context_window ?? ev.contextWindow);
473
+ let contextTokens = toNumber(context?.context_tokens ?? context?.contextTokens ?? ev.context_tokens ?? ev.contextTokens);
474
+ let contextCachedTokens = toNumber(context?.context_cached_tokens ??
475
+ context?.contextCachedTokens ??
476
+ ev.context_cached_tokens ??
477
+ ev.contextCachedTokens);
478
+ let contextRemainingTokens = toNumber(context?.context_remaining_tokens ??
479
+ context?.contextRemainingTokens ??
480
+ ev.context_remaining_tokens ??
481
+ ev.contextRemainingTokens);
482
+ const contextTruncated = toBoolean(context?.context_truncated ?? context?.contextTruncated ?? ev.context_truncated ?? ev.contextTruncated);
483
+ if (contextTokens === undefined) {
484
+ if (usage?.input_tokens !== undefined &&
485
+ usage?.cached_input_tokens !== undefined) {
486
+ contextTokens = usage.input_tokens + usage.cached_input_tokens;
487
+ }
488
+ else if (usage?.input_tokens !== undefined) {
489
+ contextTokens = usage.input_tokens;
490
+ }
491
+ }
492
+ if (contextCachedTokens === undefined && usage?.cached_input_tokens !== undefined) {
493
+ contextCachedTokens = usage.cached_input_tokens;
494
+ }
495
+ if (contextRemainingTokens === undefined &&
496
+ contextWindow !== undefined &&
497
+ contextTokens !== undefined) {
498
+ contextRemainingTokens = Math.max(0, contextWindow - contextTokens);
499
+ }
500
+ const out = {};
501
+ if (contextWindow !== undefined)
502
+ out.context_window = contextWindow;
503
+ if (contextTokens !== undefined)
504
+ out.context_tokens = contextTokens;
505
+ if (contextCachedTokens !== undefined)
506
+ out.context_cached_tokens = contextCachedTokens;
507
+ if (contextRemainingTokens !== undefined)
508
+ out.context_remaining_tokens = contextRemainingTokens;
509
+ if (contextTruncated !== undefined)
510
+ out.context_truncated = contextTruncated;
447
511
  return Object.keys(out).length ? out : null;
448
512
  }
449
513
  function normalizeItem(raw) {
@@ -840,8 +904,16 @@ export function runCodexPrompt({ prompt, system, resumeSessionId, model, reasoni
840
904
  if (usage) {
841
905
  emit({
842
906
  type: 'usage',
843
- inputTokens: usage.input_tokens,
844
- outputTokens: usage.output_tokens,
907
+ usage,
908
+ providerDetail,
909
+ });
910
+ handled = true;
911
+ }
912
+ const contextUsage = extractContextUsage(ev, usage);
913
+ if (contextUsage) {
914
+ emit({
915
+ type: 'context_usage',
916
+ contextUsage,
845
917
  providerDetail,
846
918
  });
847
919
  handled = true;
@@ -468,10 +468,20 @@ function extractUsage(ev) {
468
468
  const usage = ev.usage ?? ev.token_usage ?? ev.tokenUsage ?? ev.tokens;
469
469
  if (!usage || typeof usage !== 'object')
470
470
  return null;
471
- const toNumber = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
471
+ const toNumber = (v) => {
472
+ if (typeof v === 'number' && Number.isFinite(v))
473
+ return v;
474
+ if (typeof v === 'string' && v.trim()) {
475
+ const parsed = Number(v);
476
+ return Number.isFinite(parsed) ? parsed : undefined;
477
+ }
478
+ return undefined;
479
+ };
472
480
  const input = toNumber(usage.input_tokens ?? usage.prompt_tokens ?? usage.inputTokens ?? usage.promptTokens);
473
481
  const output = toNumber(usage.output_tokens ?? usage.completion_tokens ?? usage.outputTokens ?? usage.completionTokens);
474
482
  const total = toNumber(usage.total_tokens ?? usage.totalTokens);
483
+ const cached = toNumber(usage.cached_input_tokens ?? usage.cachedInputTokens);
484
+ const reasoning = toNumber(usage.reasoning_tokens ?? usage.reasoningTokens);
475
485
  const out = {};
476
486
  if (input !== undefined)
477
487
  out.input_tokens = input;
@@ -479,6 +489,63 @@ function extractUsage(ev) {
479
489
  out.output_tokens = output;
480
490
  if (total !== undefined)
481
491
  out.total_tokens = total;
492
+ if (cached !== undefined)
493
+ out.cached_input_tokens = cached;
494
+ if (reasoning !== undefined)
495
+ out.reasoning_tokens = reasoning;
496
+ return Object.keys(out).length ? out : null;
497
+ }
498
+ function extractContextUsage(ev, usage) {
499
+ const context = ev.context_usage ?? ev.contextUsage;
500
+ const toNumber = (v) => {
501
+ if (typeof v === 'number' && Number.isFinite(v))
502
+ return v;
503
+ if (typeof v === 'string' && v.trim()) {
504
+ const parsed = Number(v);
505
+ return Number.isFinite(parsed) ? parsed : undefined;
506
+ }
507
+ return undefined;
508
+ };
509
+ const toBoolean = (v) => typeof v === 'boolean' ? v : undefined;
510
+ const contextWindow = toNumber(context?.context_window ?? context?.contextWindow ?? ev.context_window ?? ev.contextWindow);
511
+ let contextTokens = toNumber(context?.context_tokens ?? context?.contextTokens ?? ev.context_tokens ?? ev.contextTokens);
512
+ let contextCachedTokens = toNumber(context?.context_cached_tokens ??
513
+ context?.contextCachedTokens ??
514
+ ev.context_cached_tokens ??
515
+ ev.contextCachedTokens);
516
+ let contextRemainingTokens = toNumber(context?.context_remaining_tokens ??
517
+ context?.contextRemainingTokens ??
518
+ ev.context_remaining_tokens ??
519
+ ev.contextRemainingTokens);
520
+ const contextTruncated = toBoolean(context?.context_truncated ?? context?.contextTruncated ?? ev.context_truncated ?? ev.contextTruncated);
521
+ if (contextTokens === undefined) {
522
+ if (usage?.input_tokens !== undefined &&
523
+ usage?.cached_input_tokens !== undefined) {
524
+ contextTokens = usage.input_tokens + usage.cached_input_tokens;
525
+ }
526
+ else if (usage?.input_tokens !== undefined) {
527
+ contextTokens = usage.input_tokens;
528
+ }
529
+ }
530
+ if (contextCachedTokens === undefined && usage?.cached_input_tokens !== undefined) {
531
+ contextCachedTokens = usage.cached_input_tokens;
532
+ }
533
+ if (contextRemainingTokens === undefined &&
534
+ contextWindow !== undefined &&
535
+ contextTokens !== undefined) {
536
+ contextRemainingTokens = Math.max(0, contextWindow - contextTokens);
537
+ }
538
+ const out = {};
539
+ if (contextWindow !== undefined)
540
+ out.context_window = contextWindow;
541
+ if (contextTokens !== undefined)
542
+ out.context_tokens = contextTokens;
543
+ if (contextCachedTokens !== undefined)
544
+ out.context_cached_tokens = contextCachedTokens;
545
+ if (contextRemainingTokens !== undefined)
546
+ out.context_remaining_tokens = contextRemainingTokens;
547
+ if (contextTruncated !== undefined)
548
+ out.context_truncated = contextTruncated;
482
549
  return Object.keys(out).length ? out : null;
483
550
  }
484
551
  function extractTextFromContent(content) {
@@ -787,8 +854,14 @@ export function runCursorPrompt({ prompt, system, resumeSessionId, model, repoRo
787
854
  if (usage) {
788
855
  emit({
789
856
  type: 'usage',
790
- inputTokens: usage.input_tokens,
791
- outputTokens: usage.output_tokens,
857
+ usage,
858
+ });
859
+ }
860
+ const contextUsage = extractContextUsage(ev, usage);
861
+ if (contextUsage) {
862
+ emit({
863
+ type: 'context_usage',
864
+ contextUsage,
792
865
  });
793
866
  }
794
867
  if (isErrorEvent(ev)) {
package/dist/summary.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Provider, ProviderId } from './types.js';
2
- export type SummarySource = 'prompt' | 'claude-log';
2
+ export type SummarySource = 'prompt';
3
3
  export type SummaryPayload = {
4
4
  summary: string;
5
5
  source: SummarySource;
@@ -9,6 +9,7 @@ export type SummaryPayload = {
9
9
  };
10
10
  export declare function getSummaryModel(providerId: ProviderId): string | null;
11
11
  export declare function buildSummaryPrompt(userPrompt: string, reasoning?: string): string;
12
+ export declare function buildSummaryPromptWithOverride(template: string, userPrompt: string, reasoning?: string): string;
12
13
  export declare function sanitizeSummary(raw: string): string;
13
14
  export declare function runSummaryPrompt(options: {
14
15
  provider: Provider;
package/dist/summary.js CHANGED
@@ -26,6 +26,7 @@ export function buildSummaryPrompt(userPrompt, reasoning) {
26
26
  const clippedReasoning = reasoning?.trim() ? clipText(reasoning, REASONING_MAX_CHARS) : '';
27
27
  const lines = [
28
28
  'You write ultra-short task summaries for a chat list.',
29
+ 'Do not run commands or edit files; only write the summary.',
29
30
  `Summarize the task in ${Math.max(6, SUMMARY_MAX_WORDS - 4)}-${SUMMARY_MAX_WORDS} words.`,
30
31
  'Capture the task and outcome; include key file/component/tech if present.',
31
32
  'Use a specific action verb; avoid vague verbs like "help" or "work on".',
@@ -42,6 +43,27 @@ export function buildSummaryPrompt(userPrompt, reasoning) {
42
43
  }
43
44
  return lines.join('\n');
44
45
  }
46
+ export function buildSummaryPromptWithOverride(template, userPrompt, reasoning) {
47
+ const trimmedTemplate = template.trim();
48
+ if (!trimmedTemplate)
49
+ return buildSummaryPrompt(userPrompt, reasoning);
50
+ const clipped = clipText(userPrompt.trim(), 1200);
51
+ const clippedReasoning = reasoning?.trim() ? clipText(reasoning, REASONING_MAX_CHARS) : '';
52
+ const hasMessage = trimmedTemplate.includes('{{message}}');
53
+ const hasReasoning = trimmedTemplate.includes('{{reasoning}}');
54
+ let prompt = trimmedTemplate;
55
+ if (hasMessage)
56
+ prompt = prompt.replaceAll('{{message}}', clipped);
57
+ if (hasReasoning)
58
+ prompt = prompt.replaceAll('{{reasoning}}', clippedReasoning);
59
+ if (!hasMessage) {
60
+ prompt = `${prompt}\n\nUser request:\n${clipped}`;
61
+ }
62
+ if (clippedReasoning && !hasReasoning) {
63
+ prompt = `${prompt}\n\nInitial reasoning (first lines):\n${clippedReasoning}`;
64
+ }
65
+ return prompt;
66
+ }
45
67
  export function sanitizeSummary(raw) {
46
68
  const normalized = raw
47
69
  .replace(/[\r\n]+/g, ' ')
package/dist/types.d.ts CHANGED
@@ -116,6 +116,20 @@ export interface ModelInfo {
116
116
  reasoningEfforts?: ReasoningEffort[];
117
117
  defaultReasoningEffort?: string;
118
118
  }
119
+ export type TokenUsage = {
120
+ input_tokens?: number;
121
+ output_tokens?: number;
122
+ total_tokens?: number;
123
+ cached_input_tokens?: number;
124
+ reasoning_tokens?: number;
125
+ };
126
+ export type ContextUsage = {
127
+ context_window?: number;
128
+ context_tokens?: number;
129
+ context_cached_tokens?: number;
130
+ context_remaining_tokens?: number;
131
+ context_truncated?: boolean;
132
+ };
119
133
  export interface ProviderLoginOptions {
120
134
  baseUrl?: string;
121
135
  apiKey?: string;
@@ -125,15 +139,15 @@ export interface ProviderLoginOptions {
125
139
  loginExperience?: 'embedded' | 'terminal';
126
140
  }
127
141
  export interface SessionEvent {
128
- type: 'delta' | 'final' | 'usage' | 'status' | 'error' | 'raw_line' | 'message' | 'thinking' | 'tool_call' | 'detail' | 'summary';
142
+ type: 'delta' | 'final' | 'usage' | 'context_usage' | 'status' | 'error' | 'raw_line' | 'message' | 'thinking' | 'tool_call' | 'detail' | 'summary';
129
143
  text?: string;
130
144
  message?: string;
131
145
  line?: string;
132
146
  provider?: ProviderId;
133
147
  providerDetail?: ProviderDetail;
134
148
  providerSessionId?: string | null;
135
- inputTokens?: number;
136
- outputTokens?: number;
149
+ usage?: TokenUsage;
150
+ contextUsage?: ContextUsage;
137
151
  role?: 'system' | 'user' | 'assistant';
138
152
  content?: string;
139
153
  contentParts?: unknown;
@@ -146,7 +160,7 @@ export interface SessionEvent {
146
160
  timestampMs?: number;
147
161
  cancelled?: boolean;
148
162
  summary?: string;
149
- source?: 'prompt' | 'claude-log';
163
+ source?: 'prompt';
150
164
  model?: string | null;
151
165
  createdAt?: string;
152
166
  }
@@ -195,11 +209,16 @@ export interface SessionState {
195
209
  repoRoot?: string;
196
210
  providerDetailLevel?: ProviderDetailLevel;
197
211
  systemPrompt?: string;
212
+ summaryMode?: 'auto' | 'off';
213
+ summaryPrompt?: string;
214
+ summaryNextMode?: 'auto' | 'off' | 'force';
215
+ summaryNextPrompt?: string;
216
+ summaryAutoUsed?: boolean;
198
217
  summaryRequested?: boolean;
199
218
  summarySeed?: string;
200
219
  summaryReasoning?: string;
201
220
  summary?: string | null;
202
- summarySource?: 'prompt' | 'claude-log';
221
+ summarySource?: 'prompt';
203
222
  summaryModel?: string | null;
204
223
  summaryCreatedAt?: string;
205
224
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentconnect/host",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/rayzhudev/agent-connect",