ethagent 3.1.2 → 3.3.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 (65) hide show
  1. package/README.md +15 -16
  2. package/package.json +1 -1
  3. package/src/chat/ChatScreen.tsx +25 -2
  4. package/src/chat/chatTurnOrchestrator.ts +17 -0
  5. package/src/identity/continuity/history.ts +8 -8
  6. package/src/identity/continuity/publicSkills.ts +3 -80
  7. package/src/identity/continuity/skills/frontmatter.ts +4 -1
  8. package/src/identity/continuity/skills/loadSkills.ts +73 -5
  9. package/src/identity/continuity/skills/publicSkillsSync.ts +9 -8
  10. package/src/identity/continuity/skills/scaffold.ts +1 -1
  11. package/src/identity/continuity/skills/types.ts +1 -1
  12. package/src/identity/continuity/snapshots.ts +3 -8
  13. package/src/identity/continuity/storage/defaults.ts +3 -3
  14. package/src/identity/continuity/storage/paths.ts +1 -1
  15. package/src/identity/continuity/storage/scaffold.ts +37 -25
  16. package/src/identity/continuity/storage/status.ts +11 -11
  17. package/src/identity/continuity/storage/types.ts +4 -4
  18. package/src/identity/continuity/storage.ts +4 -4
  19. package/src/identity/ens/agentRecords.ts +61 -45
  20. package/src/identity/ens/ensAutomation/read.ts +7 -10
  21. package/src/identity/ens/ensAutomation/setup.ts +10 -16
  22. package/src/identity/ens/ensAutomation/types.ts +0 -1
  23. package/src/identity/ens/ensAutomation.ts +1 -0
  24. package/src/identity/ens/ensLookup/records.ts +1 -1
  25. package/src/identity/ens/ensLookup.ts +1 -1
  26. package/src/identity/ens/erc7930.ts +48 -0
  27. package/src/identity/hub/OperationalRoutes.tsx +4 -1
  28. package/src/identity/hub/continuity/effects.ts +17 -39
  29. package/src/identity/hub/continuity/skills/NewSkillVisibilityScreen.tsx +3 -4
  30. package/src/identity/hub/continuity/skills/SkillActionsScreen.tsx +3 -4
  31. package/src/identity/hub/continuity/skills/SkillsTreeScreen.tsx +2 -1
  32. package/src/identity/hub/continuity/state.ts +1 -1
  33. package/src/identity/hub/continuity/vault.ts +16 -50
  34. package/src/identity/hub/create/effects.ts +12 -16
  35. package/src/identity/hub/ens/EnsEditFlow.tsx +19 -5
  36. package/src/identity/hub/ens/EnsEditReviewScreens.tsx +11 -11
  37. package/src/identity/hub/ens/EnsEditShared.tsx +2 -6
  38. package/src/identity/hub/ens/editCopy.ts +1 -3
  39. package/src/identity/hub/ens/transactions.ts +67 -18
  40. package/src/identity/hub/profile/effects.ts +15 -30
  41. package/src/identity/hub/profile/identity.ts +2 -4
  42. package/src/identity/hub/profile/operatorSave.ts +10 -30
  43. package/src/identity/hub/restore/RestoreFlow.tsx +9 -9
  44. package/src/identity/hub/restore/apply.ts +7 -8
  45. package/src/identity/hub/restore/helpers.ts +3 -3
  46. package/src/identity/hub/restore/recovery.ts +9 -10
  47. package/src/identity/hub/restore/resolve.ts +11 -9
  48. package/src/identity/hub/shared/components/MenuScreen.tsx +3 -3
  49. package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +2 -2
  50. package/src/identity/hub/shared/effects/sync.ts +1 -1
  51. package/src/identity/hub/transfer/TokenTransferScreens.tsx +1 -1
  52. package/src/identity/hub/transfer/effects.ts +10 -31
  53. package/src/identity/hub/useIdentityHubContinuity.ts +12 -12
  54. package/src/identity/hub/useIdentityHubController.ts +12 -3
  55. package/src/identity/registry/erc8004/metadata.ts +10 -27
  56. package/src/identity/registry/erc8004/types.ts +0 -1
  57. package/src/models/llamacpp.ts +61 -1
  58. package/src/models/llamacppPreflight.ts +5 -1
  59. package/src/runtime/compaction.ts +11 -5
  60. package/src/storage/config.ts +1 -2
  61. package/src/storage/identity.ts +3 -3
  62. package/src/storage/rewind.ts +1 -1
  63. package/src/tools/privateContinuityEditTool.ts +4 -4
  64. package/src/utils/messages.ts +1 -1
  65. package/src/utils/withRetry.ts +2 -2
@@ -26,8 +26,8 @@ export const UnlinkedIdentityScreen: React.FC<UnlinkedIdentityScreenProps> = ({
26
26
  onCancel,
27
27
  }) => {
28
28
  const options: Array<{ value: Action; label: string; hint?: string; role?: 'section' | 'utility' }> = [
29
- { value: 'load-agent', role: 'section', label: 'Load Agent' },
30
- { value: 'load-agent', label: 'Load Agent', hint: 'Reconnect this token by signing with the current owner wallet, or load a different one' },
29
+ { value: 'load-agent', role: 'section', label: 'Switch Agent' },
30
+ { value: 'load-agent', label: 'Switch Agent', hint: 'Reconnect this token by signing with the current owner wallet, or switch to a different one' },
31
31
  { value: 'open-menu', role: 'section', label: 'Identity Hub' },
32
32
  { value: 'open-menu', label: 'Open Identity Hub', hint: 'Browse local identity, continuity files, and settings without reconnecting' },
33
33
  ]
@@ -149,7 +149,7 @@ export async function markCurrentContinuityFilesPublished(
149
149
  identity: EthagentIdentity,
150
150
  publishedSources?: {
151
151
  privateFiles: ContinuityFiles
152
- publicSkills: string
152
+ agentCard: string
153
153
  skills: ContinuitySkillsTree
154
154
  },
155
155
  ): Promise<void> {
@@ -192,7 +192,7 @@ export const TokenTransferReadyScreen: React.FC<TokenTransferReadyScreenProps> =
192
192
  <Box marginTop={1} flexDirection="column">
193
193
  <Text color={theme.textSubtle}>Use this process for every ERC-8004 token transfer.</Text>
194
194
  <Text color={theme.textSubtle}>Both sender and receiver signatures can decrypt this snapshot.</Text>
195
- <Text color={theme.textSubtle}>After transfer, use Load Agent with the receiver wallet.</Text>
195
+ <Text color={theme.textSubtle}>After transfer, use Switch Agent with the receiver wallet.</Text>
196
196
  <Text color={theme.textSubtle}>{APPROVAL_GUARDRAIL}</Text>
197
197
  </Box>
198
198
  <Box marginTop={1}>
@@ -11,18 +11,10 @@ import {
11
11
  continuityVaultStatus,
12
12
  prepareSyncedSkillsTree,
13
13
  readContinuityFiles,
14
- readPublicSkillsFile,
15
- writePublicSkillsFile,
14
+ writeAgentCardFile,
16
15
  } from '../../continuity/storage.js'
17
16
  import {
18
- appendPublicSkillEntries,
19
- createAgentCard,
20
- defaultPublicSkillsProfile,
21
- serializeAgentCard,
22
- } from '../../continuity/publicSkills.js'
23
- import {
24
- derivePublicSkillEntries,
25
- syncPublicSkillsManifest,
17
+ syncAgentCardManifest,
26
18
  } from '../../continuity/skills/publicSkillsSync.js'
27
19
  import { recordPublishedContinuitySnapshot } from '../../continuity/snapshots.js'
28
20
  import { addToIpfs, DEFAULT_IPFS_API_URL, isPinataUploadUrl } from '../../storage/ipfs.js'
@@ -47,7 +39,7 @@ import { tokenTransferProgressForPhase } from './progress.js'
47
39
  import { assertTokenNotInVault } from '../custody/preflight.js'
48
40
 
49
41
  type BackupMetadata = NonNullable<EthagentIdentity['backup']>
50
- type PublicSkillsMetadata = NonNullable<EthagentIdentity['publicSkills']>
42
+ type AgentCardMetadata = NonNullable<EthagentIdentity['agentCard']>
51
43
 
52
44
  type TokenTransferResult = {
53
45
  identity: EthagentIdentity
@@ -167,20 +159,8 @@ export async function runTokenTransferSigning(
167
159
  }
168
160
  const nextIdentityForFiles: EthagentIdentity = { ...step.identity, state }
169
161
  const continuityFiles = await readContinuityFiles(nextIdentityForFiles)
170
- const publicSkillsJson = await syncPublicSkillsManifest(nextIdentityForFiles)
171
- const publicSkillsPin = await addToIpfs(DEFAULT_IPFS_API_URL, publicSkillsJson, fetch, { pinataJwt: step.pinataJwt })
172
- assertVerifiedPin(publicSkillsPin)
173
- const publicSkillEntries = await derivePublicSkillEntries(nextIdentityForFiles)
174
- const augmentedPublicProfile = appendPublicSkillEntries(
175
- defaultPublicSkillsProfile(nextIdentityForFiles),
176
- publicSkillEntries,
177
- )
178
- const agentCardPin = await addToIpfs(
179
- DEFAULT_IPFS_API_URL,
180
- serializeAgentCard(createAgentCard(augmentedPublicProfile)),
181
- fetch,
182
- { pinataJwt: step.pinataJwt },
183
- )
162
+ const agentCardJson = await syncAgentCardManifest(nextIdentityForFiles)
163
+ const agentCardPin = await addToIpfs(DEFAULT_IPFS_API_URL, agentCardJson, fetch, { pinataJwt: step.pinataJwt })
184
164
  assertVerifiedPin(agentCardPin)
185
165
  const skillsTree = await prepareSyncedSkillsTree(nextIdentityForFiles)
186
166
  const envelope = createTransferContinuitySnapshotEnvelope({
@@ -215,9 +195,8 @@ export async function runTokenTransferSigning(
215
195
  agentId: transferAgentId,
216
196
  ...(transferSnapshot ? { transferSnapshot } : {}),
217
197
  }
218
- const publicSkills: PublicSkillsMetadata = {
219
- cid: publicSkillsPin.cid,
220
- agentCardCid: agentCardPin.cid,
198
+ const agentCard: AgentCardMetadata = {
199
+ cid: agentCardPin.cid,
221
200
  updatedAt: envelope.createdAt,
222
201
  status: 'pinned',
223
202
  }
@@ -232,7 +211,7 @@ export async function runTokenTransferSigning(
232
211
  ...(uploadedImageUri ? { image: uploadedImageUri } : {}),
233
212
  }, {
234
213
  backup: { cid: snapshotCid, envelopeVersion: envelope.envelopeVersion, createdAt: envelope.createdAt, ...(transferSnapshot ? { transferSnapshot } : {}) },
235
- publicDiscovery: { skillsCid: publicSkills.cid, agentCardCid: publicSkills.agentCardCid, updatedAt: publicSkills.updatedAt },
214
+ publicDiscovery: { agentCardCid: agentCard.cid, updatedAt: agentCard.updatedAt },
236
215
  registration: { chainId: step.registry.chainId, identityRegistryAddress: step.registry.identityRegistryAddress, agentId: transferAgentId },
237
216
  ensName: nextEnsName,
238
217
  operators: operatorsPointerFromState(state, nextEnsName),
@@ -273,10 +252,10 @@ export async function runTokenTransferSigning(
273
252
  agentUri,
274
253
  metadataCid,
275
254
  backup: { ...backup, metadataCid, agentUri, txHash: tx.txHash },
276
- publicSkills,
255
+ agentCard,
277
256
  state,
278
257
  }
279
- await writePublicSkillsFile(nextIdentity, publicSkillsJson)
258
+ await writeAgentCardFile(nextIdentity, agentCardJson)
280
259
  await recordPublishedContinuitySnapshot({ identity: nextIdentity, label: 'published transfer snapshot' }).catch(() => null)
281
260
  callbacks.onTokenTransferProgress?.(null)
282
261
  return {
@@ -4,7 +4,7 @@ import { catFromIpfs, DEFAULT_IPFS_API_URL } from '../storage/ipfs.js'
4
4
  import {
5
5
  continuityVaultStatus,
6
6
  continuityWorkingTreeStatus,
7
- ensurePublicSkillsFile,
7
+ ensureAgentCardFile,
8
8
  type ContinuityWorkingTreeStatus,
9
9
  } from '../continuity/storage.js'
10
10
  import { openFileInEditor, openInFileManager } from '../continuity/editor.js'
@@ -17,7 +17,7 @@ import {
17
17
  setSkillVisibility as setSkillVisibilityStorage,
18
18
  } from '../continuity/skills/loadSkills.js'
19
19
  import type { SkillVisibility } from '../continuity/skills/types.js'
20
- import { syncPublicSkillsManifest } from '../continuity/skills/publicSkillsSync.js'
20
+ import { syncAgentCardManifest } from '../continuity/skills/publicSkillsSync.js'
21
21
  import { continuityVaultRef } from '../continuity/storage.js'
22
22
  import type { Step } from './identityHubReducer.js'
23
23
 
@@ -109,7 +109,7 @@ export function useIdentityHubContinuity({
109
109
  const id = await requireReadyVault()
110
110
  const notice = await args.run(id)
111
111
  invalidateSkillsCache(id)
112
- await syncPublicSkillsManifest(id)
112
+ await syncAgentCardManifest(id)
113
113
  const next = args.successStep
114
114
  ? args.successStep(notice)
115
115
  : { kind: 'continuity-skills-tree' as const, notice }
@@ -126,12 +126,12 @@ export function useIdentityHubContinuity({
126
126
  : 'continuity-private'
127
127
  try {
128
128
  if (kind === 'skills') {
129
- await ensurePublicSkillsFile(identity, {
130
- fallback: () => readPublishedPublicSkills(identity),
129
+ await ensureAgentCardFile(identity, {
130
+ fallback: () => readPublishedAgentCard(identity),
131
131
  })
132
132
  }
133
133
  const ref = continuityVaultRef(identity)
134
- const file = kind === 'soul' ? ref.soulPath : kind === 'memory' ? ref.memoryPath : ref.publicSkillsPath
134
+ const file = kind === 'soul' ? ref.soulPath : kind === 'memory' ? ref.memoryPath : ref.agentCardPath
135
135
  const result = await openFileInEditor(file)
136
136
  if (result.ok) {
137
137
  setStep({ kind: returnKind, editorOpened: true })
@@ -150,10 +150,10 @@ export function useIdentityHubContinuity({
150
150
  const result = await openFileInEditor(skill.absolutePath)
151
151
  invalidateSkillsCache(identity)
152
152
  try {
153
- await syncPublicSkillsManifest(identity)
153
+ await syncAgentCardManifest(identity)
154
154
  } catch (syncErr: unknown) {
155
155
  const failPrefix = result.ok ? '' : `open failed: ${result.error}; `
156
- setStep({ kind: 'continuity-skills-tree', notice: `${failPrefix}public manifest sync failed: ${(syncErr as Error).message}`, editorOpened: result.ok })
156
+ setStep({ kind: 'continuity-skills-tree', notice: `${failPrefix}agent card update failed: ${(syncErr as Error).message}`, editorOpened: result.ok })
157
157
  return
158
158
  }
159
159
  if (result.ok) {
@@ -194,7 +194,7 @@ export function useIdentityHubContinuity({
194
194
  const id = await requireReadyVault()
195
195
  const created = await createSkillFile(id, { name: normalizedName, visibility })
196
196
  invalidateSkillsCache(id)
197
- await syncPublicSkillsManifest(id)
197
+ await syncAgentCardManifest(id)
198
198
  const result = await openFileInEditor(created.absolutePath)
199
199
  if (result.ok) {
200
200
  setStep({ kind: 'continuity-skills-tree', editorOpened: true })
@@ -248,9 +248,9 @@ export function sanitizeSkillSegment(value: string): string {
248
248
  return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60)
249
249
  }
250
250
 
251
- async function readPublishedPublicSkills(identity: EthagentIdentity): Promise<string> {
252
- const cid = identity.publicSkills?.cid
253
- if (!cid) throw new Error('No saved public skills CID')
251
+ async function readPublishedAgentCard(identity: EthagentIdentity): Promise<string> {
252
+ const cid = identity.agentCard?.cid
253
+ if (!cid) throw new Error('No saved Agent Card CID')
254
254
  return new TextDecoder().decode(await catFromIpfs(
255
255
  identity.backup?.ipfsApiUrl ?? DEFAULT_IPFS_API_URL,
256
256
  cid,
@@ -240,9 +240,14 @@ export function useIdentityHubController({
240
240
  handleStepError(new Error('no agent registry configured for this identity'), backStep)
241
241
  return
242
242
  }
243
+ const isAdvanced = readCustodyMode(identity.state as Record<string, unknown> | undefined) === 'advanced'
243
244
  const vaultAddress = options?.useVault === false
244
245
  ? undefined
245
- : options?.vaultAddress ?? resolveVaultAddress(identity, config?.erc8004?.operatorVaults)
246
+ : options?.vaultAddress != null
247
+ ? options.vaultAddress
248
+ : isAdvanced
249
+ ? resolveVaultAddress(identity, config?.erc8004?.operatorVaults)
250
+ : undefined
246
251
  ;(async () => {
247
252
  const role: 'token-holder' | 'vault-level-owner' = vaultAddress ? 'vault-level-owner' : 'token-holder'
248
253
  const allowed = await guardOwnership(identity, registry, role, backStep)
@@ -260,8 +265,12 @@ export function useIdentityHubController({
260
265
  return
261
266
  }
262
267
  ;(async () => {
263
- const vaultAddress = resolveVaultAddress(identity, config?.erc8004?.operatorVaults)
264
- const allowed = await guardOwnership(identity, registry, 'vault-level-owner', backStep)
268
+ const isAdvanced = readCustodyMode(identity.state as Record<string, unknown> | undefined) === 'advanced'
269
+ const vaultAddress = isAdvanced
270
+ ? resolveVaultAddress(identity, config?.erc8004?.operatorVaults)
271
+ : undefined
272
+ const role: 'token-holder' | 'vault-level-owner' = vaultAddress ? 'vault-level-owner' : 'token-holder'
273
+ const allowed = await guardOwnership(identity, registry, role, backStep)
265
274
  if (!allowed) return
266
275
  runPublicProfilePreflight(identity, registry, callbacks, profileUpdates, backStep, vaultAddress)
267
276
  .catch((err: unknown) => handleStepError(err, backStep))
@@ -62,16 +62,15 @@ function parseTransferSnapshotMetadata(input: Record<string, unknown> | null): T
62
62
  export function parseEthagentPublicDiscoveryPointer(registration: Record<string, unknown> | null): EthagentPublicDiscoveryPointer | null {
63
63
  if (!registration) return null
64
64
  const ext = objectField(registration, 'x-ethagent') ?? objectField(registration, 'ethagent')
65
- const publicSkills = ext ? objectField(ext, 'publicSkills') : null
66
65
  const agentCard = ext ? objectField(ext, 'agentCard') : null
67
- const skillsCid = publicSkills ? stringField(publicSkills, 'cid') : undefined
68
- const agentCardCid = agentCard ? stringField(agentCard, 'cid') : undefined
69
- const updatedAt = (publicSkills ? stringField(publicSkills, 'updatedAt') : undefined)
70
- ?? (agentCard ? stringField(agentCard, 'updatedAt') : undefined)
71
- if (!skillsCid && !agentCardCid) return null
66
+ const legacyPublicSkills = ext ? objectField(ext, 'publicSkills') : null
67
+ const agentCardCid = (agentCard ? stringField(agentCard, 'cid') : undefined)
68
+ ?? (legacyPublicSkills ? stringField(legacyPublicSkills, 'cid') : undefined)
69
+ const updatedAt = (agentCard ? stringField(agentCard, 'updatedAt') : undefined)
70
+ ?? (legacyPublicSkills ? stringField(legacyPublicSkills, 'updatedAt') : undefined)
71
+ if (!agentCardCid) return null
72
72
  return {
73
- ...(skillsCid ? { skillsCid } : {}),
74
- ...(agentCardCid ? { agentCardCid } : {}),
73
+ agentCardCid,
75
74
  ...(updatedAt ? { updatedAt } : {}),
76
75
  }
77
76
  }
@@ -166,7 +165,6 @@ function serializeOperatorsPointer(pointer: EthagentOperatorsPointer): Record<st
166
165
  return {
167
166
  approvedOperatorWallets: pointer.approvedOperatorWallets.map(record => ({
168
167
  address: getAddress(record.address),
169
- ...(record.challenge ? { challenge: record.challenge } : {}),
170
168
  ...(record.verifiedAt ? { verifiedAt: record.verifiedAt } : {}),
171
169
  ...(record.restoreAccessKey ? { restoreAccessKey: serializeRestoreAccessKey(record.restoreAccessKey) } : {}),
172
170
  })),
@@ -229,13 +227,6 @@ export function withEthagentPointers(
229
227
  ...(backup.transferSnapshot ? { transferSnapshot: serializeTransferSnapshotMetadata(backup.transferSnapshot) } : {}),
230
228
  },
231
229
  } : {}),
232
- ...(publicDiscovery?.skillsCid ? {
233
- publicSkills: {
234
- cid: publicDiscovery.skillsCid,
235
- format: 'application/json',
236
- ...(updatedAt ? { updatedAt } : {}),
237
- },
238
- } : {}),
239
230
  ...(publicDiscovery?.agentCardCid ? {
240
231
  agentCard: {
241
232
  cid: publicDiscovery.agentCardCid,
@@ -285,17 +276,9 @@ function withEthagentServices(
285
276
  if (publicDiscovery?.agentCardCid) {
286
277
  const endpoint = `ipfs://${publicDiscovery.agentCardCid}`
287
278
  pushUniqueService(services, {
288
- type: 'a2a',
279
+ type: 'A2A',
289
280
  name: 'agent-card',
290
- endpoint,
291
- url: endpoint,
292
- })
293
- }
294
- if (publicDiscovery?.skillsCid) {
295
- const endpoint = `ipfs://${publicDiscovery.skillsCid}`
296
- pushUniqueService(services, {
297
- type: 'A2A-skills',
298
- name: 'public-skills',
281
+ version: '0.3.0',
299
282
  endpoint,
300
283
  url: endpoint,
301
284
  })
@@ -322,7 +305,7 @@ function isEthagentManagedService(item: unknown): boolean {
322
305
  const name = obj.name
323
306
  if (name === 'agentWallet') return true
324
307
  if (name === 'ENS') return true
325
- if (type === 'a2a' && (name === undefined || name === 'agent-card')) return true
308
+ if ((type === 'A2A' || type === 'a2a') && (name === undefined || name === 'agent-card')) return true
326
309
  return (type === 'A2A-skills' || type === 'ipfs') && name === 'public-skills'
327
310
  }
328
311
 
@@ -21,7 +21,6 @@ export type EthagentBackupPointer = {
21
21
  }
22
22
 
23
23
  export type EthagentPublicDiscoveryPointer = {
24
- skillsCid?: string
25
24
  agentCardCid?: string
26
25
  updatedAt?: string
27
26
  }
@@ -256,12 +256,68 @@ async function fetchServedModels(host: string = DEFAULT_LLAMA_HOST, timeoutMs =
256
256
  }
257
257
  }
258
258
 
259
+ let cachedLlamaCppContextSize: number | null = null
260
+ const llamaCppContextSizeListeners = new Set<(size: number) => void>()
261
+
262
+ export async function fetchLlamaCppContextSize(
263
+ host: string = DEFAULT_LLAMA_HOST,
264
+ timeoutMs = 1500,
265
+ ): Promise<number | null> {
266
+ const response = await fetchWithTimeout(`${host.replace(/\/+$/, '')}/props`, timeoutMs)
267
+ if (!response || !response.ok) return null
268
+ try {
269
+ const data = await response.json() as {
270
+ n_ctx?: unknown
271
+ default_generation_settings?: { n_ctx?: unknown }
272
+ }
273
+ const raw = typeof data.n_ctx === 'number'
274
+ ? data.n_ctx
275
+ : typeof data.default_generation_settings?.n_ctx === 'number'
276
+ ? data.default_generation_settings.n_ctx
277
+ : null
278
+ if (typeof raw === 'number' && raw > 0) {
279
+ const changed = cachedLlamaCppContextSize !== raw
280
+ cachedLlamaCppContextSize = raw
281
+ if (changed) {
282
+ for (const listener of llamaCppContextSizeListeners) {
283
+ try { listener(raw) } catch { void 0 }
284
+ }
285
+ }
286
+ return raw
287
+ }
288
+ return null
289
+ } catch {
290
+ return null
291
+ }
292
+ }
293
+
294
+ export function getCachedLlamaCppContextSize(): number | null {
295
+ return cachedLlamaCppContextSize
296
+ }
297
+
298
+ export function setCachedLlamaCppContextSize(size: number): void {
299
+ if (!(size > 0)) return
300
+ const changed = cachedLlamaCppContextSize !== size
301
+ cachedLlamaCppContextSize = size
302
+ if (changed) {
303
+ for (const listener of llamaCppContextSizeListeners) {
304
+ try { listener(size) } catch { void 0 }
305
+ }
306
+ }
307
+ }
308
+
309
+ export function onLlamaCppContextSizeChange(listener: (size: number) => void): () => void {
310
+ llamaCppContextSizeListeners.add(listener)
311
+ return () => { llamaCppContextSizeListeners.delete(listener) }
312
+ }
313
+
259
314
  export async function detectLlamaCpp(host: string = DEFAULT_LLAMA_HOST): Promise<LlamaCppStatus> {
260
315
  const [binary, serverUp] = await Promise.all([
261
316
  detectLlamaCppServerBinary(),
262
317
  isLlamaCppServerUp(host),
263
318
  ])
264
319
  const servedModels = serverUp ? await listServedModels(host) : []
320
+ if (serverUp) void fetchLlamaCppContextSize(host)
265
321
  return {
266
322
  binaryPresent: binary.path !== null,
267
323
  binaryPath: binary.path,
@@ -298,6 +354,7 @@ export async function startLlamaCppServer(args: {
298
354
  }
299
355
  }
300
356
  if (initialStatus.state === 'ready') {
357
+ void fetchLlamaCppContextSize(host)
301
358
  return { ok: true, alreadyRunning: true }
302
359
  }
303
360
  if (initialStatus.state === 'different') {
@@ -377,7 +434,10 @@ export async function startLlamaCppServer(args: {
377
434
  pollMs: args.pollMs ?? 500,
378
435
  childFailure: () => childFailure,
379
436
  })
380
- if (ready.ok) return { ok: true, alreadyRunning: false }
437
+ if (ready.ok) {
438
+ void fetchLlamaCppContextSize(host)
439
+ return { ok: true, alreadyRunning: false }
440
+ }
381
441
  if (ready.code === 'readiness-timeout') {
382
442
  return startFailure('readiness-timeout', { detail: capture() })
383
443
  }
@@ -1,4 +1,5 @@
1
1
  import {
2
+ fetchLlamaCppContextSize,
2
3
  startLlamaCppServer,
3
4
  stopLlamaCppServer,
4
5
  type LlamaCppStartFailureCode,
@@ -64,7 +65,10 @@ export async function ensureLlamaCppRunnerReady(
64
65
  servedModels: probe.models,
65
66
  }
66
67
  }
67
- if (!local.mmprojPath) return { ok: true, alreadyRunning: true }
68
+ if (!local.mmprojPath) {
69
+ void fetchLlamaCppContextSize(llamaCppServerHostFromBaseUrl(baseUrl))
70
+ return { ok: true, alreadyRunning: true }
71
+ }
68
72
  await (deps.stopServer ?? stopLlamaCppServer)().catch(() => null)
69
73
  }
70
74
 
@@ -1,6 +1,7 @@
1
1
  import type { Message, Provider } from '../providers/contracts.js'
2
2
  import { approximateTokens, messageTextContent } from '../utils/messages.js'
3
3
  import type { SessionMessage } from '../storage/sessions.js'
4
+ import { getCachedLlamaCppContextSize } from '../models/llamacpp.js'
4
5
 
5
6
  const COMPACT_SYSTEM = `Create a continuation handoff for this coding-agent conversation.
6
7
  Keep it concise but complete. Preserve the current goal, user constraints, key decisions, relevant files, tool results, pending tasks, and known failures. Do not claim unverified work was completed. No preamble.`
@@ -17,8 +18,7 @@ const CLOUD_MESSAGE_CHAR_LIMIT = 2_000
17
18
  export type CompactionStage =
18
19
  | 'preparing transcript'
19
20
  | 'compressing long context'
20
- | 'summarizing with local model'
21
- | 'summarizing with provider'
21
+ | 'summarizing transcript'
22
22
 
23
23
  export type CompactTranscriptOptions = {
24
24
  signal?: AbortSignal
@@ -60,6 +60,12 @@ export function contextWindow(model: string): number {
60
60
  export function contextWindowInfo(provider: string, model: string): ContextWindowInfo {
61
61
  const lower = model.toLowerCase()
62
62
  const providerLower = provider.toLowerCase()
63
+ if (providerLower === 'llamacpp') {
64
+ const cached = getCachedLlamaCppContextSize()
65
+ if (cached) {
66
+ return { tokens: cached, confidence: 'exact', source: 'llama.cpp /props' }
67
+ }
68
+ }
63
69
  if (lower.startsWith('qwen3:4b') || lower.startsWith('qwen3:30b') || lower.startsWith('qwen3:235b')) {
64
70
  return { tokens: 256_000, confidence: 'inferred', source: 'qwen3 long-context tag' }
65
71
  }
@@ -138,7 +144,7 @@ export async function compactTranscript(
138
144
  const signal = options.signal ?? controller!.signal
139
145
  let summary = ''
140
146
  const local = isLocalProviderId(provider.id)
141
- options.onStage?.(local ? 'summarizing with local model' : 'summarizing with provider')
147
+ options.onStage?.('summarizing transcript')
142
148
  try {
143
149
  for await (const ev of provider.complete(prompt, signal, {
144
150
  maxTokens: options.maxOutputTokens ?? (local ? LOCAL_COMPACTION_OUTPUT_TOKENS : CLOUD_COMPACTION_OUTPUT_TOKENS),
@@ -168,7 +174,7 @@ export function buildCompactionSource(
168
174
  const nonSystem = transcript.filter(m => m.role !== 'system')
169
175
  const local = isLocalProviderId(providerId)
170
176
  const tokenBudget = options.maxInputTokens ?? (local ? LOCAL_COMPACTION_INPUT_TOKENS : CLOUD_COMPACTION_INPUT_TOKENS)
171
- const charBudget = Math.max(1_000, tokenBudget * 4)
177
+ const charBudget = Math.max(1_000, tokenBudget * 3)
172
178
  const recentMessageCount = local ? LOCAL_RECENT_MESSAGE_COUNT : CLOUD_RECENT_MESSAGE_COUNT
173
179
  const messageCharLimit = local ? LOCAL_MESSAGE_CHAR_LIMIT : CLOUD_MESSAGE_CHAR_LIMIT
174
180
  const rawTokenEstimate = approximateTokens(nonSystem)
@@ -385,5 +391,5 @@ function limitCompactionText(text: string, charBudget: number): string {
385
391
  }
386
392
 
387
393
  function approximateTextTokens(text: string): number {
388
- return Math.ceil(text.length / 4)
394
+ return Math.ceil(text.length / 3)
389
395
  }
@@ -51,9 +51,8 @@ const IdentitySchema = z.object({
51
51
  createdAt: z.string(),
52
52
  })).optional(),
53
53
  }).optional(),
54
- publicSkills: z.object({
54
+ agentCard: z.object({
55
55
  cid: z.string().min(1).optional(),
56
- agentCardCid: z.string().min(1).optional(),
57
56
  updatedAt: z.string().optional(),
58
57
  status: z.enum(['pinned', 'failed', 'unknown']).optional(),
59
58
  }).optional(),
@@ -18,7 +18,7 @@ export type IdentityStatus = {
18
18
  createdAt: string
19
19
  backend: KeyBackend | 'browser-wallet'
20
20
  backup?: EthagentIdentity['backup']
21
- publicSkills?: EthagentIdentity['publicSkills']
21
+ agentCard?: EthagentIdentity['agentCard']
22
22
  source?: EthagentIdentity['source']
23
23
  agentId?: string
24
24
  chainId?: number
@@ -33,7 +33,7 @@ export async function getIdentityStatus(config?: EthagentConfig): Promise<Identi
33
33
  createdAt: resolved.identity.createdAt,
34
34
  backend: 'browser-wallet',
35
35
  backup: resolved.identity.backup,
36
- publicSkills: resolved.identity.publicSkills,
36
+ agentCard: resolved.identity.agentCard,
37
37
  source: resolved.identity.source,
38
38
  agentId: resolved.identity.agentId,
39
39
  chainId: resolved.identity.chainId,
@@ -47,7 +47,7 @@ export async function getIdentityStatus(config?: EthagentConfig): Promise<Identi
47
47
  createdAt: resolved.identity.createdAt,
48
48
  backend,
49
49
  backup: resolved.identity.backup,
50
- publicSkills: resolved.identity.publicSkills,
50
+ agentCard: resolved.identity.agentCard,
51
51
  source: resolved.identity.source,
52
52
  chainId: resolved.identity.chainId,
53
53
  }
@@ -257,7 +257,7 @@ function isIdentityMarkdownSnapshot(snapshot: RewindSnapshot): boolean {
257
257
  )
258
258
  }
259
259
 
260
- const IDENTITY_MARKDOWN_FILES = new Set(['soul.md', 'memory.md', 'skills.json'])
260
+ const IDENTITY_MARKDOWN_FILES = new Set(['soul.md', 'memory.md', 'agent-card.json'])
261
261
 
262
262
  function normalizeSnippet(input?: string): string {
263
263
  const normalized = (input ?? '').replace(/\s+/g, ' ').trim()
@@ -4,7 +4,7 @@ import {
4
4
  writePreparedPrivateContinuityEdit,
5
5
  } from '../identity/continuity/privateEdit.js'
6
6
  import { recordPrivateContinuityHistorySnapshot } from '../identity/continuity/history.js'
7
- import { readContinuityFiles, readPublicSkillsFile } from '../identity/continuity/storage.js'
7
+ import { readContinuityFiles, readAgentCardFile } from '../identity/continuity/storage.js'
8
8
  import type { Tool } from './contracts.js'
9
9
  import { formatFileChangeResult } from './fileDiff.js'
10
10
 
@@ -120,9 +120,9 @@ export const privateContinuityEditTool: Tool<typeof schema> = {
120
120
  },
121
121
  async execute(input, context) {
122
122
  const prepared = await preparePrivateContinuityEdit(input, context.config)
123
- const [previousFiles, previousPublicSkills] = await Promise.all([
123
+ const [previousFiles, previousAgentCard] = await Promise.all([
124
124
  readContinuityFiles(prepared.identity),
125
- readPublicSkillsFile(prepared.identity),
125
+ readAgentCardFile(prepared.identity),
126
126
  ])
127
127
  await recordPrivateContinuityHistorySnapshot({
128
128
  identity: prepared.identity,
@@ -131,7 +131,7 @@ export const privateContinuityEditTool: Tool<typeof schema> = {
131
131
  existedBefore: prepared.existedBefore,
132
132
  previousContent: prepared.previousContent,
133
133
  previousFiles,
134
- previousPublicSkills,
134
+ previousAgentCard,
135
135
  changeSummary: prepared.changeSummary,
136
136
  sessionId: context.checkpoint?.sessionId,
137
137
  turnId: context.checkpoint?.turnId,
@@ -33,5 +33,5 @@ export function blocksToText(blocks: MessageContentBlock[]): string {
33
33
  export function approximateTokens(messages: Message[]): number {
34
34
  let chars = 0
35
35
  for (const m of messages) chars += messageTextContent(m).length
36
- return Math.ceil(chars / 4)
36
+ return Math.ceil(chars / 3)
37
37
  }
@@ -220,7 +220,7 @@ export async function fetchWithRetry(
220
220
  && options.parseRetryHintFromBody
221
221
  ) {
222
222
  let bodyText = ''
223
- try { bodyText = await response.text() } catch { /* ignore */ }
223
+ try { bodyText = await response.text() } catch {}
224
224
  bufferedResponse = new Response(bodyText, {
225
225
  status: response.status,
226
226
  statusText: response.statusText,
@@ -240,7 +240,7 @@ export async function fetchWithRetry(
240
240
  if (!classification.retryable || attempt > policy.maxRetries) return bufferedResponse ?? response
241
241
 
242
242
  if (!bufferedResponse) {
243
- try { await response.body?.cancel() } catch { /* ignore */ }
243
+ try { await response.body?.cancel() } catch {}
244
244
  }
245
245
  const delayMs = computeBackoffMs(
246
246
  attempt,