ethagent 1.1.1 → 2.0.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 (271) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +127 -29
  3. package/package.json +16 -9
  4. package/src/app/FirstRun.tsx +192 -146
  5. package/src/app/FirstRunTimeline.tsx +47 -0
  6. package/src/app/input/AppInputProvider.tsx +1 -1
  7. package/src/app/keybindings/KeybindingProvider.tsx +1 -1
  8. package/src/chat/ChatBottomPane.tsx +0 -1
  9. package/src/chat/ChatInput.tsx +6 -6
  10. package/src/chat/ChatScreen.tsx +43 -18
  11. package/src/chat/ContextLimitView.tsx +4 -4
  12. package/src/chat/ContinuityEditReviewView.tsx +11 -17
  13. package/src/chat/ConversationStack.tsx +3 -0
  14. package/src/chat/CopyPicker.tsx +0 -1
  15. package/src/chat/MessageList.tsx +62 -45
  16. package/src/chat/PermissionPrompt.tsx +13 -9
  17. package/src/chat/PlanApprovalView.tsx +3 -3
  18. package/src/chat/ResumeView.tsx +1 -4
  19. package/src/chat/RewindView.tsx +2 -2
  20. package/src/chat/TranscriptView.tsx +6 -0
  21. package/src/chat/chatInputState.ts +1 -1
  22. package/src/chat/chatScreenUtils.ts +22 -11
  23. package/src/chat/chatSessionState.ts +2 -2
  24. package/src/chat/chatTurnOrchestrator.ts +16 -81
  25. package/src/chat/commands.ts +1 -1
  26. package/src/chat/textCursor.ts +1 -1
  27. package/src/chat/transcriptViewport.ts +2 -7
  28. package/src/cli/ResetConfirmView.tsx +1 -1
  29. package/src/cli/main.tsx +9 -3
  30. package/src/cli/preview.tsx +0 -5
  31. package/src/cli/updateNotice.ts +5 -3
  32. package/src/identity/continuity/editor.ts +7 -107
  33. package/src/identity/continuity/envelope.ts +1048 -40
  34. package/src/identity/continuity/history.ts +4 -4
  35. package/src/identity/continuity/localBackup.ts +249 -0
  36. package/src/identity/continuity/privateEdit/apply.ts +170 -0
  37. package/src/identity/continuity/privateEdit/diff.ts +82 -0
  38. package/src/identity/continuity/privateEdit/files.ts +23 -0
  39. package/src/identity/continuity/privateEdit/types.ts +28 -0
  40. package/src/identity/continuity/privateEdit.ts +10 -298
  41. package/src/identity/continuity/publicSkills.ts +8 -9
  42. package/src/identity/continuity/snapshots.ts +17 -6
  43. package/src/identity/continuity/storage/defaults.ts +111 -0
  44. package/src/identity/continuity/storage/files.ts +72 -0
  45. package/src/identity/continuity/storage/markdown.ts +81 -0
  46. package/src/identity/continuity/storage/paths.ts +24 -0
  47. package/src/identity/continuity/storage/scaffold.ts +124 -0
  48. package/src/identity/continuity/storage/status.ts +86 -0
  49. package/src/identity/continuity/storage/types.ts +27 -0
  50. package/src/identity/continuity/storage.ts +32 -507
  51. package/src/identity/continuity/zipWriter.ts +95 -0
  52. package/src/identity/crypto/backupEnvelope.ts +14 -247
  53. package/src/identity/crypto/eth.ts +7 -7
  54. package/src/identity/ens/agentRecords.ts +96 -0
  55. package/src/identity/ens/ensAutomation/contracts.ts +38 -0
  56. package/src/identity/ens/ensAutomation/delete.ts +80 -0
  57. package/src/identity/ens/ensAutomation/names.ts +14 -0
  58. package/src/identity/ens/ensAutomation/operators.ts +29 -0
  59. package/src/identity/ens/ensAutomation/read.ts +114 -0
  60. package/src/identity/ens/ensAutomation/root.ts +63 -0
  61. package/src/identity/ens/ensAutomation/setup.ts +284 -0
  62. package/src/identity/ens/ensAutomation/transactions.ts +107 -0
  63. package/src/identity/ens/ensAutomation/types.ts +126 -0
  64. package/src/identity/ens/ensAutomation.ts +29 -0
  65. package/src/identity/ens/ensLookup/client.ts +43 -0
  66. package/src/identity/ens/ensLookup/constants.ts +26 -0
  67. package/src/identity/ens/ensLookup/discovery.ts +70 -0
  68. package/src/identity/ens/ensLookup/names.ts +34 -0
  69. package/src/identity/ens/ensLookup/records.ts +45 -0
  70. package/src/identity/ens/ensLookup/resolve.ts +75 -0
  71. package/src/identity/ens/ensLookup/tokenReference.ts +17 -0
  72. package/src/identity/ens/ensLookup/types.ts +38 -0
  73. package/src/identity/ens/ensLookup/validation.ts +72 -0
  74. package/src/identity/ens/ensLookup.ts +19 -0
  75. package/src/identity/ens/ensRegistration.ts +199 -0
  76. package/src/identity/ens/resolverDelegation.ts +48 -0
  77. package/src/identity/hub/IdentityHub.tsx +13 -815
  78. package/src/identity/hub/OperationalRoutes.tsx +370 -0
  79. package/src/identity/hub/Routes.tsx +361 -0
  80. package/src/identity/hub/advancedEnsValidation.ts +45 -0
  81. package/src/identity/hub/{screens → components}/DetailsScreen.tsx +14 -8
  82. package/src/identity/hub/{screens → components}/ErrorScreen.tsx +15 -5
  83. package/src/identity/hub/components/FlowTimeline.tsx +27 -0
  84. package/src/identity/hub/components/IdentitySummary.tsx +190 -0
  85. package/src/identity/hub/components/MenuScreen.tsx +237 -0
  86. package/src/identity/hub/{screens → components}/NetworkScreen.tsx +3 -3
  87. package/src/identity/hub/{screens/RebackupStorageScreen.tsx → components/PinataJwtInput.tsx} +21 -18
  88. package/src/identity/hub/components/UnlinkedIdentityScreen.tsx +76 -0
  89. package/src/identity/hub/{screens → components}/WalletApprovalScreen.tsx +9 -8
  90. package/src/identity/hub/components/menuFlagsFromReconciliation.ts +68 -0
  91. package/src/identity/hub/effects/create.ts +310 -0
  92. package/src/identity/hub/effects/ens/flows.ts +218 -0
  93. package/src/identity/hub/effects/ens/index.ts +11 -0
  94. package/src/identity/hub/effects/ens/transactions.ts +239 -0
  95. package/src/identity/hub/effects/index.ts +74 -0
  96. package/src/identity/hub/effects/profile/profileState.ts +173 -0
  97. package/src/identity/hub/effects/publicProfile/index.ts +5 -0
  98. package/src/identity/hub/effects/publicProfile/runPublicProfileSave.ts +646 -0
  99. package/src/identity/hub/effects/rebackup/index.ts +7 -0
  100. package/src/identity/hub/effects/rebackup/operatorVault.ts +378 -0
  101. package/src/identity/hub/effects/rebackup/runRebackup.ts +451 -0
  102. package/src/identity/hub/effects/receipts.ts +46 -0
  103. package/src/identity/hub/effects/restore/apply.ts +112 -0
  104. package/src/identity/hub/effects/restore/auth.ts +159 -0
  105. package/src/identity/hub/effects/restore/discover.ts +86 -0
  106. package/src/identity/hub/effects/restore/envelopes.ts +21 -0
  107. package/src/identity/hub/effects/restore/fetch.ts +25 -0
  108. package/src/identity/hub/effects/restore/index.ts +22 -0
  109. package/src/identity/hub/effects/restore/recovery.ts +135 -0
  110. package/src/identity/hub/effects/restore/resolve.ts +102 -0
  111. package/src/identity/hub/effects/restore/restoreEffects.ts +22 -0
  112. package/src/identity/hub/effects/restore/shared.ts +91 -0
  113. package/src/identity/hub/effects/restoreAdmin.ts +93 -0
  114. package/src/identity/hub/effects/shared/profilePrep.ts +139 -0
  115. package/src/identity/hub/effects/shared/snapshot.ts +336 -0
  116. package/src/identity/hub/effects/shared/sync.ts +190 -0
  117. package/src/identity/hub/effects/token-transfer/index.ts +6 -0
  118. package/src/identity/hub/effects/token-transfer/progress.ts +59 -0
  119. package/src/identity/hub/effects/token-transfer/runTokenTransfer.ts +299 -0
  120. package/src/identity/hub/effects/types.ts +53 -0
  121. package/src/identity/hub/effects/vault/preflight.ts +50 -0
  122. package/src/identity/hub/flows/continuity/ContinuityDashboardScreen.tsx +170 -0
  123. package/src/identity/hub/flows/continuity/RebackupStorageScreen.tsx +28 -0
  124. package/src/identity/hub/flows/continuity/RecoveryConfirmScreen.tsx +104 -0
  125. package/src/identity/hub/flows/continuity/SavePromptScreen.tsx +49 -0
  126. package/src/identity/hub/{screens → flows/create}/CreateFlow.tsx +61 -62
  127. package/src/identity/hub/flows/custody/CustodyEditFlow.tsx +347 -0
  128. package/src/identity/hub/flows/custody/custodyEffects.ts +321 -0
  129. package/src/identity/hub/flows/custody/custodyFlowActions.ts +236 -0
  130. package/src/identity/hub/flows/custody/custodyFlowEffects.ts +163 -0
  131. package/src/identity/hub/flows/custody/custodyFlowHelpers.ts +25 -0
  132. package/src/identity/hub/flows/custody/custodyFlowRoutes.tsx +239 -0
  133. package/src/identity/hub/flows/custody/custodyFlowTypes.ts +45 -0
  134. package/src/identity/hub/flows/custody/useCustodyFlow.tsx +25 -0
  135. package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +336 -0
  136. package/src/identity/hub/flows/ens/EnsEditFlow.tsx +397 -0
  137. package/src/identity/hub/flows/ens/EnsEditMaintenanceScreens.tsx +332 -0
  138. package/src/identity/hub/flows/ens/EnsEditReviewScreens.tsx +471 -0
  139. package/src/identity/hub/flows/ens/EnsEditRunners.tsx +198 -0
  140. package/src/identity/hub/flows/ens/EnsEditShared.tsx +162 -0
  141. package/src/identity/hub/flows/ens/EnsEditSimpleScreens.tsx +518 -0
  142. package/src/identity/hub/flows/ens/IdentityHubEnsFlow.tsx +299 -0
  143. package/src/identity/hub/flows/ens/OperatorWalletsScreen.tsx +398 -0
  144. package/src/identity/hub/flows/ens/ensEditCopy.ts +117 -0
  145. package/src/identity/hub/flows/ens/ensEditTypes.ts +91 -0
  146. package/src/identity/hub/flows/profile/EditProfileFlow.tsx +271 -0
  147. package/src/identity/hub/flows/restore/RestoreFlow.tsx +324 -0
  148. package/src/identity/hub/flows/restore/useRestoreFlowEffects.ts +77 -0
  149. package/src/identity/hub/{screens → flows/settings}/StorageCredentialScreen.tsx +25 -43
  150. package/src/identity/hub/flows/token-transfer/IdentityHubTokenTransferFlow.tsx +162 -0
  151. package/src/identity/hub/flows/token-transfer/TokenTransferScreens.tsx +256 -0
  152. package/src/identity/hub/identityHubReducer.ts +166 -101
  153. package/src/identity/hub/model/continuity.ts +94 -0
  154. package/src/identity/hub/model/copy.ts +35 -0
  155. package/src/identity/hub/model/custody.ts +54 -0
  156. package/src/identity/hub/model/ens.ts +49 -0
  157. package/src/identity/hub/model/errors.ts +140 -0
  158. package/src/identity/hub/model/format.ts +15 -0
  159. package/src/identity/hub/model/identity.ts +94 -0
  160. package/src/identity/hub/model/network.ts +32 -0
  161. package/src/identity/hub/model/transfer.ts +57 -0
  162. package/src/identity/hub/operatorWallets.ts +131 -0
  163. package/src/identity/hub/reconciliation/agentReconciliation/hook.ts +46 -0
  164. package/src/identity/hub/reconciliation/agentReconciliation/ownership.ts +129 -0
  165. package/src/identity/hub/reconciliation/agentReconciliation/run.ts +302 -0
  166. package/src/identity/hub/reconciliation/agentReconciliation/types.ts +17 -0
  167. package/src/identity/hub/reconciliation/index.ts +21 -0
  168. package/src/identity/hub/reconciliation/useAgentReconciliation.ts +10 -0
  169. package/src/identity/hub/reconciliation/walletSetup.ts +220 -0
  170. package/src/identity/hub/txGuard.ts +51 -0
  171. package/src/identity/hub/types.ts +17 -0
  172. package/src/identity/hub/useIdentityHubContinuity.ts +136 -0
  173. package/src/identity/hub/useIdentityHubController.ts +396 -0
  174. package/src/identity/hub/useIdentityHubSideEffects.ts +309 -0
  175. package/src/identity/hub/utils.ts +79 -0
  176. package/src/identity/identityCompat.ts +34 -0
  177. package/src/identity/profile/agentIcon.ts +61 -0
  178. package/src/identity/profile/imagePicker.ts +12 -12
  179. package/src/identity/registry/erc8004/abi.ts +14 -0
  180. package/src/identity/registry/erc8004/chains.ts +150 -0
  181. package/src/identity/registry/erc8004/client.ts +11 -0
  182. package/src/identity/registry/erc8004/discovery.ts +511 -0
  183. package/src/identity/registry/erc8004/metadata.ts +335 -0
  184. package/src/identity/registry/erc8004/ownership.ts +121 -0
  185. package/src/identity/registry/erc8004/preflight.ts +123 -0
  186. package/src/identity/registry/erc8004/transactions.ts +77 -0
  187. package/src/identity/registry/erc8004/types.ts +88 -0
  188. package/src/identity/registry/erc8004/uri.ts +59 -0
  189. package/src/identity/registry/erc8004/utils.ts +58 -0
  190. package/src/identity/registry/erc8004.ts +53 -1106
  191. package/src/identity/registry/fieldParsers.ts +28 -0
  192. package/src/identity/registry/operatorVault/bytecode.ts +98 -0
  193. package/src/identity/registry/operatorVault/constants.ts +38 -0
  194. package/src/identity/registry/operatorVault/read.ts +246 -0
  195. package/src/identity/registry/operatorVault/transactions.ts +81 -0
  196. package/src/identity/registry/operatorVault.ts +44 -0
  197. package/src/identity/storage/ipfs.ts +26 -24
  198. package/src/identity/wallet/browserWallet/gas.ts +41 -0
  199. package/src/identity/wallet/browserWallet/html.ts +106 -0
  200. package/src/identity/wallet/browserWallet/http.ts +28 -0
  201. package/src/identity/wallet/browserWallet/requestServer.ts +106 -0
  202. package/src/identity/wallet/browserWallet/requests.ts +191 -0
  203. package/src/identity/wallet/browserWallet/session.ts +325 -0
  204. package/src/identity/wallet/browserWallet/types.ts +192 -0
  205. package/src/identity/wallet/browserWallet/validation.ts +74 -0
  206. package/src/identity/wallet/browserWallet.ts +30 -393
  207. package/src/identity/wallet/page/constants.ts +5 -0
  208. package/src/identity/wallet/page/controller.ts +251 -0
  209. package/src/identity/wallet/page/copy.ts +340 -0
  210. package/src/identity/wallet/page/grainient.ts +278 -0
  211. package/src/identity/wallet/page/html.ts +28 -0
  212. package/src/identity/wallet/page/markup.ts +50 -0
  213. package/src/identity/wallet/page/state.ts +9 -0
  214. package/src/identity/wallet/page/styles/base.ts +259 -0
  215. package/src/identity/wallet/page/styles/components.ts +262 -0
  216. package/src/identity/wallet/page/styles/index.ts +5 -0
  217. package/src/identity/wallet/page/styles/responsive.ts +247 -0
  218. package/src/identity/wallet/page/types.ts +47 -0
  219. package/src/identity/wallet/page/view.ts +535 -0
  220. package/src/identity/wallet/page/walletProvider.ts +70 -0
  221. package/src/identity/wallet/page.tsx +38 -0
  222. package/src/identity/wallet/walletPurposeCompat.ts +27 -0
  223. package/src/mcp/manager.ts +0 -1
  224. package/src/models/ModelPicker.tsx +36 -30
  225. package/src/models/catalog.ts +5 -2
  226. package/src/models/huggingface.ts +9 -9
  227. package/src/models/llamacpp.ts +13 -13
  228. package/src/models/modelDisplay.ts +75 -0
  229. package/src/models/modelPickerOptions.ts +16 -3
  230. package/src/models/modelRecommendation.ts +0 -1
  231. package/src/providers/errors.ts +16 -0
  232. package/src/providers/gemini.ts +252 -39
  233. package/src/providers/registry.ts +2 -2
  234. package/src/providers/retry.ts +1 -1
  235. package/src/runtime/sessionMode.ts +1 -1
  236. package/src/runtime/systemPrompt.ts +2 -0
  237. package/src/runtime/toolExecution.ts +18 -22
  238. package/src/runtime/toolIntent.ts +0 -20
  239. package/src/runtime/turn.ts +0 -92
  240. package/src/storage/atomicWrite.ts +4 -1
  241. package/src/storage/config.ts +181 -5
  242. package/src/storage/identity.ts +9 -3
  243. package/src/storage/secrets.ts +2 -2
  244. package/src/tools/bashSafety.ts +8 -0
  245. package/src/tools/changeDirectoryTool.ts +1 -1
  246. package/src/tools/deleteFileTool.ts +4 -4
  247. package/src/tools/editTool.ts +4 -4
  248. package/src/tools/editUtils.ts +5 -5
  249. package/src/tools/privateContinuityEditTool.ts +4 -5
  250. package/src/tools/privateContinuityReadTool.ts +1 -2
  251. package/src/tools/registry.ts +30 -0
  252. package/src/tools/writeFileTool.ts +5 -5
  253. package/src/ui/BrandSplash.tsx +20 -85
  254. package/src/ui/ProgressBar.tsx +3 -5
  255. package/src/ui/Select.tsx +21 -9
  256. package/src/ui/Spinner.tsx +38 -3
  257. package/src/ui/Surface.tsx +3 -3
  258. package/src/ui/TextInput.tsx +191 -29
  259. package/src/ui/theme.ts +7 -34
  260. package/src/utils/openExternal.ts +21 -0
  261. package/src/utils/withRetry.ts +47 -3
  262. package/src/identity/hub/identityHubEffects.ts +0 -937
  263. package/src/identity/hub/identityHubModel.ts +0 -291
  264. package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +0 -144
  265. package/src/identity/hub/screens/EditProfileFlow.tsx +0 -145
  266. package/src/identity/hub/screens/IdentitySummary.tsx +0 -90
  267. package/src/identity/hub/screens/MenuScreen.tsx +0 -117
  268. package/src/identity/hub/screens/RecoveryConfirmScreen.tsx +0 -87
  269. package/src/identity/hub/screens/RestoreFlow.tsx +0 -206
  270. package/src/identity/wallet/wallet-page/wallet.html +0 -1202
  271. /package/src/identity/hub/{screens → components}/BusyScreen.tsx +0 -0
@@ -1,17 +1,32 @@
1
1
  import { getKey } from '../storage/secrets.js'
2
- import type { Message, Provider, ProviderCompleteOptions, StreamEvent } from './contracts.js'
2
+ import type { Message, MessageContentBlock, Provider, ProviderCompleteOptions, StreamEvent } from './contracts.js'
3
3
  import { ProviderError } from './contracts.js'
4
4
  import { providerErrorFromResponse } from './errors.js'
5
5
  import { fetchWithRetryStreamEvents } from './retry.js'
6
6
  import { iterSseFrames } from './sse.js'
7
- import { messageTextContent } from '../utils/messages.js'
7
+
8
+ export type GeminiToolDefinition = {
9
+ name: string
10
+ description: string
11
+ parameters: {
12
+ type: 'object'
13
+ properties?: Record<string, unknown>
14
+ required?: string[]
15
+ }
16
+ }
17
+
18
+ type GeminiPart = {
19
+ text?: string
20
+ functionCall?: {
21
+ name?: string
22
+ args?: Record<string, unknown>
23
+ }
24
+ }
8
25
 
9
26
  type GeminiChunk = {
10
27
  candidates?: Array<{
11
28
  content?: {
12
- parts?: Array<{
13
- text?: string
14
- }>
29
+ parts?: GeminiPart[]
15
30
  }
16
31
  finishReason?: string
17
32
  }>
@@ -24,15 +39,45 @@ type GeminiChunk = {
24
39
  }
25
40
  }
26
41
 
42
+ type GeminiContentPart =
43
+ | { text: string }
44
+ | { functionCall: { name: string; args: Record<string, unknown> } }
45
+ | { functionResponse: { name: string; response: Record<string, unknown> } }
46
+
47
+ type GeminiContent = {
48
+ role: 'user' | 'model'
49
+ parts: GeminiContentPart[]
50
+ }
51
+
52
+ type GeminiPayload = {
53
+ contents: GeminiContent[]
54
+ systemInstruction?: { parts: Array<{ text: string }> }
55
+ generationConfig?: { maxOutputTokens?: number }
56
+ tools?: Array<{ functionDeclarations: GeminiToolDefinition[] }>
57
+ toolConfig?: { functionCallingConfig: { mode: 'AUTO' } }
58
+ }
59
+
60
+ type DoneStopReason = 'end_turn' | 'tool_use' | 'max_tokens' | 'stop_sequence' | 'unknown'
61
+
27
62
  const READ_TIMEOUT_MS = 45_000
28
63
 
64
+ export type GeminiQuotaInfo = {
65
+ retryAfterMs?: number
66
+ metric?: string
67
+ quotaValue?: string
68
+ quotaId?: string
69
+ }
70
+
29
71
  export class GeminiProvider implements Provider {
30
72
  readonly id = 'gemini' as const
31
73
  readonly model: string
32
- readonly supportsTools = false
74
+ readonly supportsTools: boolean
75
+ private readonly tools: GeminiToolDefinition[]
33
76
 
34
- constructor(opts: { model: string }) {
77
+ constructor(opts: { model: string; tools?: GeminiToolDefinition[] }) {
35
78
  this.model = opts.model
79
+ this.tools = opts.tools ?? []
80
+ this.supportsTools = this.tools.length > 0
36
81
  }
37
82
 
38
83
  async *complete(
@@ -40,16 +85,17 @@ export class GeminiProvider implements Provider {
40
85
  signal: AbortSignal,
41
86
  options: ProviderCompleteOptions = {},
42
87
  ): AsyncIterable<StreamEvent> {
43
- const apiKey = await getKey('gemini')
88
+ const rawApiKey = await getKey('gemini')
89
+ const apiKey = rawApiKey?.trim()
44
90
  if (!apiKey) {
45
91
  const error = new ProviderError('missing API key for gemini (/doctor to verify)')
46
92
  yield { type: 'error', message: error.message }
47
93
  return
48
94
  }
49
95
 
50
- const payload = buildGeminiPayload(messages, options)
96
+ const payload = buildGeminiPayload(messages, this.tools, options)
51
97
  const modelName = this.model.replace(/^models\//, '')
52
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(modelName)}:streamGenerateContent?alt=sse&key=${encodeURIComponent(apiKey)}`
98
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(modelName)}:streamGenerateContent?alt=sse`
53
99
 
54
100
  let response: Response
55
101
  try {
@@ -58,6 +104,7 @@ export class GeminiProvider implements Provider {
58
104
  headers: {
59
105
  'content-type': 'application/json',
60
106
  accept: 'text/event-stream',
107
+ 'x-goog-api-key': apiKey,
61
108
  },
62
109
  body: JSON.stringify(payload),
63
110
  }, { signal })
@@ -79,6 +126,9 @@ export class GeminiProvider implements Provider {
79
126
 
80
127
  let inputTokens: number | undefined
81
128
  let outputTokens: number | undefined
129
+ let stopReason: DoneStopReason = 'unknown'
130
+ let toolCallIndex = 0
131
+ let sawToolCall = false
82
132
 
83
133
  try {
84
134
  for await (const frame of iterSseFrames(response.body, signal, READ_TIMEOUT_MS)) {
@@ -94,9 +144,26 @@ export class GeminiProvider implements Provider {
94
144
  throw new ProviderError(`prompt blocked: ${blockedReason.toLowerCase()}`)
95
145
  }
96
146
 
97
- const parts = parsed.candidates?.[0]?.content?.parts ?? []
147
+ const candidate = parsed.candidates?.[0]
148
+ const parts = candidate?.content?.parts ?? []
98
149
  for (const part of parts) {
99
- if (part.text) yield { type: 'text', delta: part.text }
150
+ if (part.text) {
151
+ yield { type: 'text', delta: part.text }
152
+ continue
153
+ }
154
+ if (part.functionCall?.name) {
155
+ const id = `gemini-tool-${toolCallIndex}`
156
+ toolCallIndex += 1
157
+ sawToolCall = true
158
+ const name = part.functionCall.name
159
+ const input = part.functionCall.args ?? {}
160
+ yield { type: 'tool_use_start', id, name }
161
+ yield { type: 'tool_use_stop', id, name, input }
162
+ }
163
+ }
164
+
165
+ if (candidate?.finishReason) {
166
+ stopReason = normalizeFinishReason(candidate.finishReason, sawToolCall)
100
167
  }
101
168
 
102
169
  inputTokens = parsed.usageMetadata?.promptTokenCount ?? inputTokens
@@ -109,44 +176,190 @@ export class GeminiProvider implements Provider {
109
176
  }
110
177
 
111
178
  if (signal.aborted) return
112
- yield { type: 'done', inputTokens, outputTokens }
179
+ if (sawToolCall) stopReason = 'tool_use'
180
+ yield { type: 'done', inputTokens, outputTokens, stopReason }
113
181
  }
114
182
  }
115
183
 
116
- function buildGeminiPayload(messages: Message[], options: ProviderCompleteOptions = {}): {
117
- contents: Array<{
118
- role: 'user' | 'model'
119
- parts: Array<{ text: string }>
120
- }>
121
- systemInstruction?: {
122
- parts: Array<{ text: string }>
123
- }
124
- generationConfig?: {
125
- maxOutputTokens?: number
126
- }
127
- } {
184
+ export function buildGeminiPayload(
185
+ messages: Message[],
186
+ tools: GeminiToolDefinition[] = [],
187
+ options: ProviderCompleteOptions = {},
188
+ ): GeminiPayload {
128
189
  const systemParts: string[] = []
129
- const contents: Array<{
130
- role: 'user' | 'model'
131
- parts: Array<{ text: string }>
132
- }> = []
190
+ const contents: GeminiContent[] = []
191
+ const toolUseNamesById = new Map<string, string>()
133
192
 
134
193
  for (const message of messages) {
135
- const text = messageTextContent(message).trim()
136
- if (!text) continue
194
+ const blocks = normalizeBlocks(message.content)
195
+ if (blocks.length === 0) continue
196
+
137
197
  if (message.role === 'system') {
138
- systemParts.push(text)
198
+ const systemText = blocks
199
+ .filter((block): block is Extract<MessageContentBlock, { type: 'text' }> => block.type === 'text')
200
+ .map(block => block.text)
201
+ .join('\n\n')
202
+ .trim()
203
+ if (systemText) systemParts.push(systemText)
204
+ continue
205
+ }
206
+
207
+ if (message.role === 'assistant') {
208
+ const parts: GeminiContentPart[] = []
209
+ for (const block of blocks) {
210
+ if (block.type === 'text') {
211
+ parts.push({ text: block.text })
212
+ } else if (block.type === 'tool_use') {
213
+ toolUseNamesById.set(block.id, block.name)
214
+ parts.push({ functionCall: { name: block.name, args: block.input } })
215
+ }
216
+ }
217
+ if (parts.length > 0) contents.push({ role: 'model', parts })
139
218
  continue
140
219
  }
141
- contents.push({
142
- role: message.role === 'assistant' ? 'model' : 'user',
143
- parts: [{ text }],
144
- })
220
+
221
+ const parts: GeminiContentPart[] = []
222
+ for (const block of blocks) {
223
+ if (block.type === 'text') {
224
+ parts.push({ text: block.text })
225
+ } else if (block.type === 'tool_result') {
226
+ const name = toolUseNamesById.get(block.toolUseId) ?? 'unknown'
227
+ const response: Record<string, unknown> = block.isError
228
+ ? { content: block.content, isError: true }
229
+ : { content: block.content }
230
+ parts.push({ functionResponse: { name, response } })
231
+ }
232
+ }
233
+ if (parts.length > 0) contents.push({ role: 'user', parts })
145
234
  }
146
235
 
236
+ const payload: GeminiPayload = { contents }
237
+ if (systemParts.length > 0) {
238
+ payload.systemInstruction = { parts: [{ text: systemParts.join('\n\n') }] }
239
+ }
240
+ if (options.maxTokens) {
241
+ payload.generationConfig = { maxOutputTokens: options.maxTokens }
242
+ }
243
+ if (tools.length > 0) {
244
+ payload.tools = [{ functionDeclarations: tools }]
245
+ payload.toolConfig = { functionCallingConfig: { mode: 'AUTO' } }
246
+ }
247
+ return payload
248
+ }
249
+
250
+ function normalizeBlocks(content: Message['content']): MessageContentBlock[] {
251
+ if (typeof content === 'string') {
252
+ const text = content.trim()
253
+ return text ? [{ type: 'text', text }] : []
254
+ }
255
+ return content.filter(block => {
256
+ if (block.type === 'text') return block.text.trim().length > 0
257
+ return true
258
+ })
259
+ }
260
+
261
+ function normalizeFinishReason(reason: string, sawToolCall: boolean): DoneStopReason {
262
+ if (sawToolCall) return 'tool_use'
263
+ switch (reason) {
264
+ case 'STOP':
265
+ return 'end_turn'
266
+ case 'MAX_TOKENS':
267
+ return 'max_tokens'
268
+ case 'STOP_SEQUENCE':
269
+ return 'stop_sequence'
270
+ default:
271
+ return 'unknown'
272
+ }
273
+ }
274
+
275
+ type GeminiQuotaInfoInternal = GeminiQuotaInfo & { kind: 'quota-failure' | 'rate-limit' }
276
+
277
+ function readGeminiQuotaInfo(bodyText: string): GeminiQuotaInfoInternal | undefined {
278
+ let body: unknown
279
+ try {
280
+ body = JSON.parse(bodyText)
281
+ } catch {
282
+ return undefined
283
+ }
284
+ if (Array.isArray(body)) body = body[0]
285
+ if (!body || typeof body !== 'object') return undefined
286
+
287
+ const error = (body as { error?: unknown }).error
288
+ if (!error || typeof error !== 'object') return undefined
289
+
290
+ const details = (error as { details?: unknown }).details
291
+ if (!Array.isArray(details)) return undefined
292
+
293
+ let retryAfterMs: number | undefined
294
+ let metric: string | undefined
295
+ let quotaValue: string | undefined
296
+ let quotaId: string | undefined
297
+ let isQuotaFailure = false
298
+
299
+ for (const detail of details) {
300
+ if (!detail || typeof detail !== 'object') continue
301
+ const type = (detail as { '@type'?: unknown })['@type']
302
+ if (typeof type !== 'string') continue
303
+
304
+ if (/RetryInfo$/.test(type)) {
305
+ const delay = (detail as { retryDelay?: unknown }).retryDelay
306
+ const parsed = parseGoogleDurationMs(delay)
307
+ if (parsed !== undefined) retryAfterMs = parsed
308
+ } else if (/QuotaFailure$/.test(type)) {
309
+ isQuotaFailure = true
310
+ const violations = (detail as { violations?: unknown }).violations
311
+ if (Array.isArray(violations) && violations.length > 0) {
312
+ const first = violations[0] as Record<string, unknown> | undefined
313
+ if (first) {
314
+ const m = first.metric
315
+ const qv = first.quotaValue
316
+ const qi = first.quotaId
317
+ if (typeof m === 'string') metric = m
318
+ if (typeof qv === 'string') quotaValue = qv
319
+ if (typeof qi === 'string') quotaId = qi
320
+ }
321
+ }
322
+ }
323
+ }
324
+
325
+ if (!isQuotaFailure && retryAfterMs === undefined) return undefined
147
326
  return {
148
- contents,
149
- systemInstruction: systemParts.length > 0 ? { parts: [{ text: systemParts.join('\n\n') }] } : undefined,
150
- generationConfig: options.maxTokens ? { maxOutputTokens: options.maxTokens } : undefined,
327
+ kind: isQuotaFailure ? 'quota-failure' : 'rate-limit',
328
+ retryAfterMs,
329
+ metric,
330
+ quotaValue,
331
+ quotaId,
332
+ }
333
+ }
334
+
335
+ function parseGoogleDurationMs(value: unknown): number | undefined {
336
+ if (typeof value !== 'string') return undefined
337
+ const m = /^(\d+(?:\.\d+)?)s$/.exec(value.trim())
338
+ if (!m) return undefined
339
+ const seconds = Number(m[1])
340
+ return Number.isFinite(seconds) ? Math.round(seconds * 1000) : undefined
341
+ }
342
+
343
+ export async function formatGeminiRateLimitMessage(response: Response): Promise<string | undefined> {
344
+ let bodyText = ''
345
+ try { bodyText = await response.text() } catch { return undefined }
346
+ const info = readGeminiQuotaInfo(bodyText)
347
+ if (!info) return undefined
348
+ const exhausted = info.kind === 'quota-failure'
349
+ const isFreeTier = info.metric ? /free_tier/i.test(info.metric) : false
350
+ const parts = [exhausted ? 'gemini quota hit' : 'gemini rate limit']
351
+ if (info.quotaValue) {
352
+ parts.push(isFreeTier ? `(free-tier cap: ${info.quotaValue})` : `(cap: ${info.quotaValue})`)
353
+ } else if (isFreeTier) {
354
+ parts.push('(free-tier cap)')
355
+ }
356
+ if (info.retryAfterMs !== undefined) {
357
+ const seconds = Math.ceil(info.retryAfterMs / 1000)
358
+ parts.push(`— retry in ~${seconds}s`)
359
+ } else if (exhausted && isFreeTier) {
360
+ parts.push('— enable billing on the API key\'s project, or /model to switch')
361
+ } else if (exhausted) {
362
+ parts.push('— /model to switch providers, or wait for the quota window to reset')
151
363
  }
364
+ return parts.join(' ')
152
365
  }
@@ -6,7 +6,7 @@ import type { SessionMode } from '../runtime/sessionMode.js'
6
6
  import { AnthropicProvider } from './anthropic.js'
7
7
  import { GeminiProvider } from './gemini.js'
8
8
  import { OpenAIChatProvider } from './openai-chat.js'
9
- import { anthropicTools, openAITools } from '../tools/registry.js'
9
+ import { anthropicTools, geminiTools, openAITools } from '../tools/registry.js'
10
10
  import { openAIBaseUrlFor } from '../models/catalog.js'
11
11
  import type { Tool } from '../tools/contracts.js'
12
12
 
@@ -37,6 +37,6 @@ export function createProvider(config: EthagentConfig, options: { mode?: Session
37
37
  case 'anthropic':
38
38
  return new AnthropicProvider({ model: config.model, tools: anthropicTools(mode, toolContext) })
39
39
  case 'gemini':
40
- return new GeminiProvider({ model: config.model })
40
+ return new GeminiProvider({ model: config.model, tools: geminiTools(mode, toolContext) })
41
41
  }
42
42
  }
@@ -54,5 +54,5 @@ export async function* fetchWithRetryStreamEvents(
54
54
  await fetchPromise
55
55
  if (settled?.state === 'resolved') return settled.response
56
56
  if (settled?.state === 'rejected') throw settled.error
57
- throw new Error('fetch retry completed without a response')
57
+ throw new Error('Fetch retry completed without a response')
58
58
  }
@@ -24,7 +24,7 @@ export function nextSessionMode(mode: SessionMode): SessionMode {
24
24
  }
25
25
 
26
26
  export function sessionModeLabel(mode: SessionMode): string {
27
- return mode === 'plan' ? 'plan mode' : mode === 'accept-edits' ? 'accept edits on' : ''
27
+ return mode === 'plan' ? 'Plan mode' : mode === 'accept-edits' ? 'Accept edits on' : ''
28
28
  }
29
29
 
30
30
  export function modePolicy(mode: PolicyMode): ModePolicy {
@@ -98,6 +98,7 @@ function buildToolEnabledPrompt(ctx: SystemPromptContext): string {
98
98
  ]
99
99
  : ['No agent identity is linked in this session. Do not attempt private identity continuity edits; ask the user to create or load an agent first.']),
100
100
  'Use `run_bash` **only** when true shell execution is necessary.',
101
+ 'Never use `run_bash` to produce conversational text. Do not call `echo`, `printf`, or similar to emit your reply — write it as your assistant text. Bash is for actions that need a real shell, not for generating words.',
101
102
  '**CWD CONTINUITY**: The working directory below is authoritative. After `change_directory` succeeds, use the new path as the base for subsequent actions.',
102
103
  'Do not lag behind the CWD. Edit/read relative to the *current* working directory.',
103
104
  'If asked for a complete application/site/game, **create the files yourself**. Do not hand back copy-paste templates.',
@@ -112,6 +113,7 @@ function buildToolEnabledPrompt(ctx: SystemPromptContext): string {
112
113
  'Local Model Tool Discipline',
113
114
  [
114
115
  '**PROTOCOL**: Emit tool calls in the native tool-call protocol. Do NOT describe the call in prose first, and do NOT print a JSON blob inside markdown as a substitute for an actual tool call.',
116
+ '**NO ECHO REPLIES**: Never call `run_bash` with `echo`, `printf`, or any command whose only purpose is to print your reply. Reply directly in your assistant text. Bash is for real shell actions only.',
115
117
  '**NO FAKE COMPLETIONS**: NEVER claim you have updated or created a file if you have not used the edit tools. Talk is cheap, use the tools.',
116
118
  'One tool call per response when a tool is needed. Wait for the tool result before deciding the next step.',
117
119
  ...(ctx.hasIdentity
@@ -24,10 +24,6 @@ import {
24
24
  import type { MessageRow } from '../chat/MessageList.js'
25
25
  import { modePolicy, toPermissionMode, type SessionMode } from './sessionMode.js'
26
26
 
27
- // ---------------------------------------------------------------------------
28
- // Tool execution with permission gating
29
- // ---------------------------------------------------------------------------
30
-
31
27
  export type ToolExecutorOptions = {
32
28
  name: string
33
29
  input: Record<string, unknown>
@@ -188,10 +184,6 @@ function formatToolParseError(err: unknown, toolName?: string): string {
188
184
  return withToolHint((err as Error).message || 'tool input did not match the required schema')
189
185
  }
190
186
 
191
- // ---------------------------------------------------------------------------
192
- // Pending tool-use runner (per turn)
193
- // ---------------------------------------------------------------------------
194
-
195
187
  export type PendingToolUse = {
196
188
  id: string
197
189
  name: string
@@ -239,9 +231,16 @@ export async function runPendingToolUses(args: {
239
231
  const completedTools: CompletedToolUse[] = []
240
232
 
241
233
  for (const toolUse of args.pendingToolUses) {
234
+ const rowId = args.nextRowId()
242
235
  args.updateRows(prev => [
243
236
  ...prev,
244
- { role: 'tool_use', id: args.nextRowId(), name: toolUse.name, summary: toolUse.name, input: summarizeToolInput(toolUse.input) },
237
+ {
238
+ role: 'tool_call',
239
+ id: rowId,
240
+ name: toolUse.name,
241
+ summary: toolUse.name,
242
+ input: summarizeToolInput(toolUse.input),
243
+ },
245
244
  ])
246
245
  await args.persistTurnMessage({
247
246
  version: 2,
@@ -266,7 +265,7 @@ export async function runPendingToolUses(args: {
266
265
  }
267
266
 
268
267
  await args.applySessionRule(sessionRule, persistRule)
269
- await recordToolResult(args, toolUse, result)
268
+ await recordToolResult(args, toolUse, result, rowId)
270
269
  }
271
270
 
272
271
  return { cancelled: false, completedTools }
@@ -279,25 +278,22 @@ async function recordToolResult(
279
278
  >,
280
279
  toolUse: PendingToolUse,
281
280
  result: ToolResult,
281
+ rowId: string,
282
282
  ): Promise<void> {
283
- args.updateRows(prev => [
284
- ...prev,
285
- {
286
- role: 'tool_result',
287
- id: args.nextRowId(),
288
- name: toolUse.name,
289
- summary: result.summary,
290
- content: toolResultContentForRow(toolUse.name, result.content, !result.ok),
291
- isError: !result.ok,
292
- },
293
- ])
283
+ const isError = !result.ok
284
+ const resultContent = toolResultContentForRow(toolUse.name, result.content, isError)
285
+ args.updateRows(prev => prev.map(row =>
286
+ row.role === 'tool_call' && row.id === rowId
287
+ ? { ...row, result: { content: resultContent, summary: result.summary, isError } }
288
+ : row,
289
+ ))
294
290
  await args.persistTurnMessage({
295
291
  version: 2,
296
292
  role: 'tool_result',
297
293
  toolUseId: toolUse.id,
298
294
  name: toolUse.name,
299
295
  content: result.content,
300
- isError: !result.ok,
296
+ isError,
301
297
  createdAt: args.nowIso(),
302
298
  turnId: args.turnId,
303
299
  })
@@ -8,18 +8,6 @@ export type ToolIntent = {
8
8
  reason: string
9
9
  }
10
10
 
11
- /**
12
- * detectDirectToolIntent — typed detection for high-confidence direct
13
- * filesystem requests. Returns the first matching intent, or null for
14
- * ambiguous or multi-step engineering requests so the model handles those.
15
- *
16
- * Covers:
17
- * - change_directory: "cd into identity", "go to src/identity"
18
- * - list_directory: "list files", "show what's here", "ls"
19
- * - read_file: "read package.json", "open/show/cat <file>"
20
- *
21
- * Returns null for anything else.
22
- */
23
11
  export function detectDirectToolIntent(userText: string): ToolIntent | null {
24
12
  const uses = directToolUsesForUserText(userText)
25
13
  if (uses.length === 0) return null
@@ -28,14 +16,6 @@ export function detectDirectToolIntent(userText: string): ToolIntent | null {
28
16
  return { name: first.name, input: first.input, reason }
29
17
  }
30
18
 
31
- /**
32
- * validateAssistantTextAgainstTurnEvidence — checks whether assistant prose
33
- * claims workspace state that isn't backed by a tool result from the active
34
- * turn.
35
- *
36
- * Returns 'ok' if the text is safe (no unsupported claims), or 'needs-tool'
37
- * if the text claims state that has no matching tool evidence.
38
- */
39
19
  export function validateAssistantTextAgainstTurnEvidence(
40
20
  text: string,
41
21
  evidence: ToolEvidence[],