ethagent 2.1.1 → 2.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.
- package/package.json +2 -1
- package/src/app/FirstRun.tsx +1 -7
- package/src/app/FirstRunTimeline.tsx +1 -1
- package/src/auth/openaiOAuth/credentials.ts +47 -0
- package/src/auth/openaiOAuth/crypto.ts +23 -0
- package/src/auth/openaiOAuth/index.ts +238 -0
- package/src/auth/openaiOAuth/landingPage.ts +125 -0
- package/src/auth/openaiOAuth/listener.ts +151 -0
- package/src/auth/openaiOAuth/refresh.ts +70 -0
- package/src/auth/openaiOAuth/shared.ts +115 -0
- package/src/chat/ChatBottomPane.tsx +20 -11
- package/src/chat/ChatScreen.tsx +160 -35
- package/src/chat/ConversationStack.tsx +1 -1
- package/src/chat/MessageList.tsx +185 -72
- package/src/chat/SessionStatus.tsx +3 -1
- package/src/chat/chatScreenUtils.ts +11 -15
- package/src/chat/chatSessionState.ts +3 -2
- package/src/chat/chatTurnOrchestrator.ts +1 -7
- package/src/chat/commands.ts +28 -27
- package/src/chat/display/DiffView.tsx +193 -0
- package/src/chat/display/SyntaxText.tsx +192 -0
- package/src/chat/display/toolCallDisplay.ts +103 -0
- package/src/chat/display/toolResultDisplay.ts +19 -0
- package/src/chat/{ChatInput.tsx → input/ChatInput.tsx} +36 -23
- package/src/chat/{TranscriptView.tsx → transcript/TranscriptView.tsx} +24 -50
- package/src/chat/{transcriptViewport.ts → transcript/transcriptViewport.ts} +12 -30
- package/src/chat/{ContextLimitView.tsx → views/ContextLimitView.tsx} +3 -3
- package/src/chat/{ContinuityEditReviewView.tsx → views/ContinuityEditReviewView.tsx} +11 -3
- package/src/chat/{CopyPicker.tsx → views/CopyPicker.tsx} +4 -5
- package/src/chat/{PermissionPrompt.tsx → views/PermissionPrompt.tsx} +16 -17
- package/src/chat/{PermissionsView.tsx → views/PermissionsView.tsx} +6 -6
- package/src/chat/{PlanApprovalView.tsx → views/PlanApprovalView.tsx} +4 -4
- package/src/chat/{ResumeView.tsx → views/ResumeView.tsx} +35 -35
- package/src/chat/views/RewindView.tsx +410 -0
- package/src/identity/continuity/privateEdit/diff.ts +2 -78
- package/src/identity/ens/agentRecords.ts +5 -19
- package/src/identity/ens/ensAutomation/setup.ts +0 -1
- package/src/identity/ens/ensAutomation/types.ts +0 -1
- package/src/identity/hub/OperationalRoutes.tsx +23 -32
- package/src/identity/hub/Routes.tsx +13 -13
- package/src/identity/hub/{flows/continuity → continuity}/ContinuityDashboardScreen.tsx +9 -9
- package/src/identity/hub/{flows/continuity → continuity}/RebackupStorageScreen.tsx +2 -2
- package/src/identity/hub/{flows/continuity → continuity}/RecoveryConfirmScreen.tsx +5 -5
- package/src/identity/hub/{flows/continuity → continuity}/SavePromptScreen.tsx +5 -5
- package/src/identity/hub/{effects/rebackup/runRebackup.ts → continuity/effects.ts} +19 -19
- package/src/identity/hub/{effects/rebackup → continuity}/index.ts +1 -1
- package/src/identity/hub/{effects/shared → continuity}/snapshot.ts +8 -8
- package/src/identity/hub/{effects/rebackup → continuity}/vault.ts +15 -15
- package/src/identity/hub/{flows/create → create}/CreateFlow.tsx +13 -13
- package/src/identity/hub/{effects/create.ts → create/effects.ts} +4 -4
- package/src/identity/hub/{flows/custody → custody}/CustodyEditFlow.tsx +10 -48
- package/src/identity/hub/{flows/custody/custodyFlowActions.ts → custody/actions.ts} +11 -9
- package/src/identity/hub/{flows/custody/custodyFlowHelpers.ts → custody/helpers.ts} +4 -4
- package/src/identity/hub/{effects/vault → custody}/preflight.ts +5 -5
- package/src/identity/hub/{flows/custody/custodyFlowRoutes.tsx → custody/routes.tsx} +8 -8
- package/src/identity/hub/{flows/custody/custodyEffects.ts → custody/transactions.ts} +9 -9
- package/src/identity/hub/{flows/custody/custodyFlowTypes.ts → custody/types.ts} +6 -6
- package/src/identity/hub/{flows/custody/custodyFlowEffects.ts → custody/useCustodyEffects.ts} +7 -7
- package/src/identity/hub/{flows/custody → custody}/useCustodyFlow.tsx +5 -5
- package/src/identity/hub/ens/EnsEditAdvancedScreens.tsx +241 -0
- package/src/identity/hub/{flows/ens → ens}/EnsEditFlow.tsx +27 -82
- package/src/identity/hub/{flows/ens → ens}/EnsEditMaintenanceScreens.tsx +25 -65
- package/src/identity/hub/{flows/ens → ens}/EnsEditReviewScreens.tsx +12 -30
- package/src/identity/hub/ens/EnsEditRunners.tsx +62 -0
- package/src/identity/hub/{flows/ens → ens}/EnsEditShared.tsx +15 -14
- package/src/identity/hub/{flows/ens → ens}/EnsEditSimpleScreens.tsx +68 -217
- package/src/identity/hub/{flows/ens/IdentityHubEnsFlow.tsx → ens/EnsFlow.tsx} +18 -11
- package/src/identity/hub/{flows/ens/OperatorWalletsScreen.tsx → ens/EnsOperatorWalletsScreen.tsx} +17 -48
- package/src/identity/hub/{advancedEnsValidation.ts → ens/advancedEnsValidation.ts} +2 -2
- package/src/identity/hub/{flows/ens/ensEditCopy.ts → ens/editCopy.ts} +4 -4
- package/src/identity/hub/{effects/ens/flows.ts → ens/effects.ts} +7 -7
- package/src/identity/hub/{effects/ens → ens}/index.ts +1 -1
- package/src/identity/hub/{model/ens.ts → ens/state.ts} +1 -1
- package/src/identity/hub/{effects/ens → ens}/transactions.ts +232 -232
- package/src/identity/hub/{flows/ens/ensEditTypes.ts → ens/types.ts} +12 -26
- package/src/identity/hub/identityHubReducer.ts +3 -3
- package/src/identity/hub/{flows/profile → profile}/EditProfileFlow.tsx +17 -10
- package/src/identity/hub/{effects/publicProfile/runPublicProfileSave.ts → profile/effects.ts} +55 -177
- package/src/identity/hub/{model → profile}/identity.ts +3 -3
- package/src/identity/hub/{effects/profile/profileState.ts → profile/state.ts} +181 -173
- package/src/identity/hub/{flows/restore → restore}/RestoreFlow.tsx +21 -21
- package/src/identity/hub/{effects/restore → restore}/apply.ts +10 -10
- package/src/identity/hub/{effects/restore → restore}/auth.ts +7 -7
- package/src/identity/hub/{effects/restore → restore}/discover.ts +6 -6
- package/src/identity/hub/{effects/restore → restore}/envelopes.ts +2 -2
- package/src/identity/hub/{effects/restore → restore}/fetch.ts +3 -3
- package/src/identity/hub/{effects/restore/shared.ts → restore/helpers.ts} +6 -6
- package/src/identity/hub/{effects/restore → restore}/recovery.ts +10 -10
- package/src/identity/hub/{effects/restore → restore}/resolve.ts +4 -4
- package/src/identity/hub/restore/restoreAdmin.ts +34 -0
- package/src/identity/hub/{flows/restore/useRestoreFlowEffects.ts → restore/useRestoreEffects.ts} +5 -5
- package/src/identity/hub/{flows/settings → settings}/StorageCredentialScreen.tsx +5 -5
- package/src/identity/hub/{components → shared/components}/BusyScreen.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/DetailsScreen.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/ErrorScreen.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/FlowTimeline.tsx +1 -1
- package/src/identity/hub/{components → shared/components}/IdentitySummary.tsx +16 -11
- package/src/identity/hub/{components → shared/components}/MenuScreen.tsx +8 -9
- package/src/identity/hub/{components → shared/components}/NetworkScreen.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/PinataJwtInput.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/UnlinkedIdentityScreen.tsx +5 -5
- package/src/identity/hub/{components → shared/components}/WalletApprovalScreen.tsx +6 -6
- package/src/identity/hub/{components → shared/components}/menuFlagsFromReconciliation.ts +2 -4
- package/src/identity/hub/{effects/shared → shared/effects}/profilePrep.ts +1 -1
- package/src/identity/hub/{effects → shared/effects}/receipts.ts +2 -2
- package/src/identity/hub/{effects/shared → shared/effects}/sync.ts +6 -47
- package/src/identity/hub/{effects → shared/effects}/types.ts +3 -3
- package/src/identity/hub/{model → shared/model}/copy.ts +2 -2
- package/src/identity/hub/{model → shared/model}/errors.ts +5 -5
- package/src/identity/hub/{model → shared/model}/network.ts +3 -3
- package/src/identity/hub/{operatorWallets.ts → shared/operatorWallets.ts} +1 -1
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/hook.ts +1 -2
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/ownership.ts +2 -2
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/run.ts +7 -40
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/types.ts +0 -4
- package/src/identity/hub/{reconciliation → shared/reconciliation}/index.ts +0 -7
- package/src/identity/hub/shared/reconciliation/walletSetup.ts +27 -0
- package/src/identity/hub/{utils.ts → shared/utils.ts} +5 -5
- package/src/identity/hub/{flows/token-transfer/IdentityHubTokenTransferFlow.tsx → transfer/TokenTransferFlow.tsx} +8 -8
- package/src/identity/hub/{flows/token-transfer → transfer}/TokenTransferScreens.tsx +14 -14
- package/src/identity/hub/{effects/token-transfer/runTokenTransfer.ts → transfer/effects.ts} +16 -16
- package/src/identity/hub/{effects/token-transfer → transfer}/progress.ts +1 -1
- package/src/identity/hub/useIdentityHubController.ts +11 -11
- package/src/identity/hub/useIdentityHubSideEffects.ts +11 -11
- package/src/identity/wallet/browserWallet/types.ts +0 -5
- package/src/identity/wallet/page/copy.ts +1 -31
- package/src/identity/wallet/walletPurposeCompat.ts +0 -2
- package/src/models/ModelPicker.tsx +248 -8
- package/src/models/catalog.ts +29 -1
- package/src/models/modelPickerOptions.ts +12 -10
- package/src/models/providerDisplay.ts +16 -0
- package/src/providers/errors.ts +6 -4
- package/src/providers/openai-chat.ts +2 -1
- package/src/providers/openai-responses-format.ts +156 -0
- package/src/providers/openai-responses.ts +276 -0
- package/src/providers/registry.ts +85 -8
- package/src/runtime/sessionMode.ts +1 -1
- package/src/runtime/systemPrompt.ts +4 -2
- package/src/runtime/toolExecution.ts +9 -6
- package/src/runtime/turn.ts +29 -1
- package/src/storage/rewind.ts +20 -0
- package/src/storage/secrets.ts +4 -1
- package/src/storage/sessions.ts +2 -1
- package/src/tools/bashSafety.ts +7 -3
- package/src/tools/bashTool.ts +1 -1
- package/src/tools/contracts.ts +3 -0
- package/src/tools/deleteFileTool.ts +8 -3
- package/src/tools/editTool.ts +10 -5
- package/src/tools/fileDiff.ts +261 -0
- package/src/tools/privateContinuityEditTool.ts +11 -1
- package/src/tools/writeFileTool.ts +8 -3
- package/src/ui/Spinner.tsx +25 -3
- package/src/ui/TextInput.tsx +2 -2
- package/src/ui/theme.ts +17 -0
- package/src/utils/clipboard.ts +10 -7
- package/src/utils/openExternal.ts +20 -10
- package/src/chat/RewindView.tsx +0 -386
- package/src/chat/toolResultDisplay.ts +0 -8
- package/src/identity/ens/ensRegistration.ts +0 -199
- package/src/identity/hub/effects/index.ts +0 -74
- package/src/identity/hub/effects/publicProfile/index.ts +0 -5
- package/src/identity/hub/effects/restore/restoreEffects.ts +0 -22
- package/src/identity/hub/effects/restoreAdmin.ts +0 -93
- package/src/identity/hub/effects/token-transfer/index.ts +0 -6
- package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +0 -336
- package/src/identity/hub/flows/ens/EnsEditRunners.tsx +0 -198
- package/src/identity/hub/reconciliation/walletSetup.ts +0 -220
- /package/src/chat/{chatInputState.ts → input/chatInputState.ts} +0 -0
- /package/src/chat/{chatPaste.ts → input/chatPaste.ts} +0 -0
- /package/src/chat/{textCursor.ts → input/textCursor.ts} +0 -0
- /package/src/identity/hub/{model/continuity.ts → continuity/state.ts} +0 -0
- /package/src/identity/hub/{model/custody.ts → custody/state.ts} +0 -0
- /package/src/identity/hub/{effects/restore → restore}/index.ts +0 -0
- /package/src/identity/hub/{model → shared/model}/format.ts +0 -0
- /package/src/identity/hub/{reconciliation → shared/reconciliation}/useAgentReconciliation.ts +0 -0
- /package/src/identity/hub/{txGuard.ts → shared/txGuard.ts} +0 -0
- /package/src/identity/hub/{model/transfer.ts → transfer/state.ts} +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { Message, MessageContentBlock } from './contracts.js'
|
|
2
|
+
import { messageTextContent } from '../utils/messages.js'
|
|
3
|
+
import type { OpenAIToolDefinition } from './openai-chat.js'
|
|
4
|
+
|
|
5
|
+
export type ResponsesInputContent =
|
|
6
|
+
| { type: 'input_text'; text: string }
|
|
7
|
+
| { type: 'output_text'; text: string }
|
|
8
|
+
|
|
9
|
+
export type ResponsesInputItem =
|
|
10
|
+
| { type: 'message'; role: 'user' | 'assistant'; content: ResponsesInputContent[] }
|
|
11
|
+
| { type: 'function_call'; id?: string; call_id: string; name: string; arguments: string }
|
|
12
|
+
| { type: 'function_call_output'; call_id: string; output: string }
|
|
13
|
+
|
|
14
|
+
export type ResponsesTool = {
|
|
15
|
+
type: 'function'
|
|
16
|
+
name: string
|
|
17
|
+
description: string
|
|
18
|
+
parameters: Record<string, unknown>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ResponsesRequestBody = {
|
|
22
|
+
model: string
|
|
23
|
+
input: ResponsesInputItem[]
|
|
24
|
+
instructions?: string
|
|
25
|
+
tools?: ResponsesTool[]
|
|
26
|
+
tool_choice?: 'auto' | 'none' | 'required'
|
|
27
|
+
parallel_tool_calls?: boolean
|
|
28
|
+
stream: true
|
|
29
|
+
store: false
|
|
30
|
+
max_output_tokens?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function buildResponsesBody(args: {
|
|
34
|
+
model: string
|
|
35
|
+
messages: Message[]
|
|
36
|
+
tools: OpenAIToolDefinition[]
|
|
37
|
+
maxOutputTokens?: number
|
|
38
|
+
}): ResponsesRequestBody {
|
|
39
|
+
const { instructions, items } = splitMessages(args.messages)
|
|
40
|
+
const body: ResponsesRequestBody = {
|
|
41
|
+
model: args.model,
|
|
42
|
+
input: items,
|
|
43
|
+
stream: true,
|
|
44
|
+
store: false,
|
|
45
|
+
}
|
|
46
|
+
if (instructions) body.instructions = instructions
|
|
47
|
+
if (args.tools.length > 0) {
|
|
48
|
+
body.tools = args.tools.map(tool => ({
|
|
49
|
+
type: 'function' as const,
|
|
50
|
+
name: tool.function.name,
|
|
51
|
+
description: tool.function.description,
|
|
52
|
+
parameters: tool.function.parameters as Record<string, unknown>,
|
|
53
|
+
}))
|
|
54
|
+
body.parallel_tool_calls = true
|
|
55
|
+
body.tool_choice = 'auto'
|
|
56
|
+
}
|
|
57
|
+
if (args.maxOutputTokens !== undefined) {
|
|
58
|
+
body.max_output_tokens = args.maxOutputTokens
|
|
59
|
+
}
|
|
60
|
+
return body
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function splitMessages(messages: Message[]): {
|
|
64
|
+
instructions?: string
|
|
65
|
+
items: ResponsesInputItem[]
|
|
66
|
+
} {
|
|
67
|
+
const instructions: string[] = []
|
|
68
|
+
const items: ResponsesInputItem[] = []
|
|
69
|
+
|
|
70
|
+
for (const message of messages) {
|
|
71
|
+
if (message.role === 'system') {
|
|
72
|
+
const text = typeof message.content === 'string'
|
|
73
|
+
? message.content
|
|
74
|
+
: messageTextContent(message)
|
|
75
|
+
if (text) instructions.push(text)
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (message.role === 'user') {
|
|
80
|
+
const blocks = normalizeBlocks(message.content)
|
|
81
|
+
const toolResults = blocks.filter(isToolResultBlock)
|
|
82
|
+
if (toolResults.length > 0) {
|
|
83
|
+
for (const block of toolResults) {
|
|
84
|
+
items.push({
|
|
85
|
+
type: 'function_call_output',
|
|
86
|
+
call_id: block.toolUseId,
|
|
87
|
+
output: block.content,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
const remainingText = blocks
|
|
91
|
+
.filter(isTextBlock)
|
|
92
|
+
.map(block => block.text)
|
|
93
|
+
.join('')
|
|
94
|
+
if (remainingText) {
|
|
95
|
+
items.push({
|
|
96
|
+
type: 'message',
|
|
97
|
+
role: 'user',
|
|
98
|
+
content: [{ type: 'input_text', text: remainingText }],
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
const text = blocks.filter(isTextBlock).map(block => block.text).join('')
|
|
104
|
+
if (text) {
|
|
105
|
+
items.push({
|
|
106
|
+
type: 'message',
|
|
107
|
+
role: 'user',
|
|
108
|
+
content: [{ type: 'input_text', text }],
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const blocks = normalizeBlocks(message.content)
|
|
115
|
+
const text = blocks.filter(isTextBlock).map(block => block.text).join('')
|
|
116
|
+
if (text) {
|
|
117
|
+
items.push({
|
|
118
|
+
type: 'message',
|
|
119
|
+
role: 'assistant',
|
|
120
|
+
content: [{ type: 'output_text', text }],
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
for (const block of blocks.filter(isToolUseBlock)) {
|
|
124
|
+
items.push({
|
|
125
|
+
type: 'function_call',
|
|
126
|
+
call_id: block.id,
|
|
127
|
+
name: block.name,
|
|
128
|
+
arguments: JSON.stringify(block.input),
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
instructions: instructions.length > 0 ? instructions.join('\n\n') : undefined,
|
|
135
|
+
items,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizeBlocks(content: Message['content']): MessageContentBlock[] {
|
|
140
|
+
if (typeof content === 'string') {
|
|
141
|
+
return content ? [{ type: 'text', text: content }] : []
|
|
142
|
+
}
|
|
143
|
+
return content
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isTextBlock(block: MessageContentBlock): block is Extract<MessageContentBlock, { type: 'text' }> {
|
|
147
|
+
return block.type === 'text'
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isToolUseBlock(block: MessageContentBlock): block is Extract<MessageContentBlock, { type: 'tool_use' }> {
|
|
151
|
+
return block.type === 'tool_use'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isToolResultBlock(block: MessageContentBlock): block is Extract<MessageContentBlock, { type: 'tool_result' }> {
|
|
155
|
+
return block.type === 'tool_result'
|
|
156
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import type { ProviderId } from '../storage/config.js'
|
|
2
|
+
import type { Message, Provider, ProviderCompleteOptions, StreamEvent } from './contracts.js'
|
|
3
|
+
import { ProviderError } from './contracts.js'
|
|
4
|
+
import { providerErrorFromResponse } from './errors.js'
|
|
5
|
+
import { fetchWithRetryStreamEvents } from './retry.js'
|
|
6
|
+
import { iterSseEvents } from './sse.js'
|
|
7
|
+
import { buildResponsesBody } from './openai-responses-format.js'
|
|
8
|
+
import type { OpenAIToolDefinition } from './openai-chat.js'
|
|
9
|
+
|
|
10
|
+
const READ_TIMEOUT_MS = 45_000
|
|
11
|
+
|
|
12
|
+
type DoneStopReason = 'end_turn' | 'tool_use' | 'max_tokens' | 'stop_sequence' | 'unknown'
|
|
13
|
+
|
|
14
|
+
export type OpenAIResponsesProviderOptions = {
|
|
15
|
+
model: string
|
|
16
|
+
baseUrl: string
|
|
17
|
+
accessToken: string
|
|
18
|
+
accountId?: string
|
|
19
|
+
originator?: string
|
|
20
|
+
tools?: OpenAIToolDefinition[]
|
|
21
|
+
maxRetries?: number
|
|
22
|
+
refresh?: () => Promise<string>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type StreamingToolCall = {
|
|
26
|
+
callId: string
|
|
27
|
+
name: string
|
|
28
|
+
inputJson: string
|
|
29
|
+
started: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class OpenAIResponsesProvider implements Provider {
|
|
33
|
+
readonly id: ProviderId = 'openai'
|
|
34
|
+
readonly model: string
|
|
35
|
+
readonly supportsTools: boolean
|
|
36
|
+
private accessToken: string
|
|
37
|
+
private readonly baseUrl: string
|
|
38
|
+
private readonly accountId?: string
|
|
39
|
+
private readonly originator: string
|
|
40
|
+
private readonly tools: OpenAIToolDefinition[]
|
|
41
|
+
private readonly maxRetries?: number
|
|
42
|
+
private readonly refresh?: () => Promise<string>
|
|
43
|
+
|
|
44
|
+
constructor(opts: OpenAIResponsesProviderOptions) {
|
|
45
|
+
this.model = opts.model
|
|
46
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, '')
|
|
47
|
+
this.accessToken = opts.accessToken
|
|
48
|
+
this.accountId = opts.accountId
|
|
49
|
+
this.originator = opts.originator ?? 'codex_cli_rs'
|
|
50
|
+
this.tools = opts.tools ?? []
|
|
51
|
+
this.maxRetries = opts.maxRetries
|
|
52
|
+
this.refresh = opts.refresh
|
|
53
|
+
this.supportsTools = this.tools.length > 0
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async *complete(
|
|
57
|
+
messages: Message[],
|
|
58
|
+
signal: AbortSignal,
|
|
59
|
+
options: ProviderCompleteOptions = {},
|
|
60
|
+
): AsyncIterable<StreamEvent> {
|
|
61
|
+
if (!this.accessToken) {
|
|
62
|
+
const error = new ProviderError('missing OAuth access token for openai (sign in again via the model picker)')
|
|
63
|
+
yield { type: 'error', message: error.message }
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let attempt = 0
|
|
68
|
+
while (true) {
|
|
69
|
+
attempt += 1
|
|
70
|
+
const body = JSON.stringify(buildResponsesBody({
|
|
71
|
+
model: this.model,
|
|
72
|
+
messages,
|
|
73
|
+
tools: this.tools,
|
|
74
|
+
maxOutputTokens: options.maxTokens,
|
|
75
|
+
}))
|
|
76
|
+
|
|
77
|
+
let response: Response
|
|
78
|
+
try {
|
|
79
|
+
response = yield* fetchWithRetryStreamEvents(`${this.baseUrl}/responses`, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: this.requestHeaders(),
|
|
82
|
+
body,
|
|
83
|
+
}, { signal, maxRetries: this.maxRetries, rateLimitResetProvider: 'openai-compatible' })
|
|
84
|
+
} catch (err: unknown) {
|
|
85
|
+
if (signal.aborted) return
|
|
86
|
+
yield { type: 'error', message: networkErrorMessage(this.baseUrl, err) }
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (response.status === 401 && this.refresh && attempt === 1) {
|
|
91
|
+
try {
|
|
92
|
+
this.accessToken = await this.refresh()
|
|
93
|
+
continue
|
|
94
|
+
} catch (refreshErr) {
|
|
95
|
+
const message = refreshErr instanceof Error ? refreshErr.message : String(refreshErr)
|
|
96
|
+
yield { type: 'error', message: `OpenAI sign-in expired and refresh failed: ${message}` }
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
const error = await providerErrorFromResponse('openai', response)
|
|
103
|
+
yield { type: 'error', message: error.message }
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
if (!response.body) {
|
|
107
|
+
yield { type: 'error', message: 'empty response body' }
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
yield* this.parseStream(response.body, signal)
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private requestHeaders(): Record<string, string> {
|
|
117
|
+
const headers: Record<string, string> = {
|
|
118
|
+
'Content-Type': 'application/json',
|
|
119
|
+
Accept: 'text/event-stream',
|
|
120
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
121
|
+
originator: this.originator,
|
|
122
|
+
}
|
|
123
|
+
if (this.accountId) headers['chatgpt-account-id'] = this.accountId
|
|
124
|
+
return headers
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private async *parseStream(body: ReadableStream<Uint8Array>, signal: AbortSignal): AsyncIterable<StreamEvent> {
|
|
128
|
+
const toolCalls = new Map<string, StreamingToolCall>()
|
|
129
|
+
let inputTokens: number | undefined
|
|
130
|
+
let outputTokens: number | undefined
|
|
131
|
+
let stopReason: DoneStopReason = 'unknown'
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
for await (const frame of iterSseEvents(body, signal, READ_TIMEOUT_MS)) {
|
|
135
|
+
const eventName = frame.event ?? ''
|
|
136
|
+
if (!frame.data || frame.data === '[DONE]') continue
|
|
137
|
+
let parsed: Record<string, unknown>
|
|
138
|
+
try {
|
|
139
|
+
parsed = JSON.parse(frame.data) as Record<string, unknown>
|
|
140
|
+
} catch {
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
switch (eventName) {
|
|
145
|
+
case 'response.output_text.delta': {
|
|
146
|
+
const delta = typeof parsed.delta === 'string' ? parsed.delta : ''
|
|
147
|
+
if (delta) yield { type: 'text', delta }
|
|
148
|
+
break
|
|
149
|
+
}
|
|
150
|
+
case 'response.reasoning_summary_text.delta':
|
|
151
|
+
case 'response.reasoning.delta':
|
|
152
|
+
case 'response.reasoning_text.delta': {
|
|
153
|
+
const delta = typeof parsed.delta === 'string' ? parsed.delta : ''
|
|
154
|
+
if (delta) yield { type: 'thinking', delta }
|
|
155
|
+
break
|
|
156
|
+
}
|
|
157
|
+
case 'response.output_item.added': {
|
|
158
|
+
const item = (parsed.item ?? {}) as Record<string, unknown>
|
|
159
|
+
if (item.type === 'function_call') {
|
|
160
|
+
const callId = pickString(item.call_id) ?? pickString(item.id) ?? `tool-${toolCalls.size}`
|
|
161
|
+
const name = pickString(item.name) ?? ''
|
|
162
|
+
toolCalls.set(callId, { callId, name, inputJson: '', started: false })
|
|
163
|
+
if (name) {
|
|
164
|
+
toolCalls.get(callId)!.started = true
|
|
165
|
+
yield { type: 'tool_use_start', id: callId, name }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
break
|
|
169
|
+
}
|
|
170
|
+
case 'response.function_call_arguments.delta': {
|
|
171
|
+
const callId = resolveCallId(parsed, toolCalls)
|
|
172
|
+
const delta = typeof parsed.delta === 'string' ? parsed.delta : ''
|
|
173
|
+
if (!callId || !delta) break
|
|
174
|
+
const existing = toolCalls.get(callId)
|
|
175
|
+
if (!existing) break
|
|
176
|
+
existing.inputJson += delta
|
|
177
|
+
yield { type: 'tool_use_delta', id: callId, delta }
|
|
178
|
+
break
|
|
179
|
+
}
|
|
180
|
+
case 'response.output_item.done': {
|
|
181
|
+
const item = (parsed.item ?? {}) as Record<string, unknown>
|
|
182
|
+
if (item.type === 'function_call') {
|
|
183
|
+
const callId = pickString(item.call_id) ?? pickString(item.id)
|
|
184
|
+
if (!callId) break
|
|
185
|
+
const existing = toolCalls.get(callId)
|
|
186
|
+
const name = pickString(item.name) ?? existing?.name ?? ''
|
|
187
|
+
const argsJson = pickString(item.arguments) ?? existing?.inputJson ?? ''
|
|
188
|
+
stopReason = 'tool_use'
|
|
189
|
+
yield {
|
|
190
|
+
type: 'tool_use_stop',
|
|
191
|
+
id: callId,
|
|
192
|
+
name,
|
|
193
|
+
input: parseToolArguments(argsJson),
|
|
194
|
+
}
|
|
195
|
+
toolCalls.delete(callId)
|
|
196
|
+
}
|
|
197
|
+
break
|
|
198
|
+
}
|
|
199
|
+
case 'response.completed': {
|
|
200
|
+
const usage = (parsed.response as { usage?: Record<string, unknown> } | undefined)?.usage
|
|
201
|
+
const tokens = readUsage(usage)
|
|
202
|
+
if (tokens.input !== undefined) inputTokens = tokens.input
|
|
203
|
+
if (tokens.output !== undefined) outputTokens = tokens.output
|
|
204
|
+
if (stopReason !== 'tool_use') stopReason = 'end_turn'
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
case 'response.failed':
|
|
208
|
+
case 'response.error':
|
|
209
|
+
case 'error': {
|
|
210
|
+
const error = parsed.error as { message?: string } | undefined
|
|
211
|
+
const message = error?.message ?? 'Responses API error'
|
|
212
|
+
yield { type: 'error', message }
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
case 'response.incomplete': {
|
|
216
|
+
const reason = pickString((parsed.response as { incomplete_details?: { reason?: string } } | undefined)?.incomplete_details?.reason)
|
|
217
|
+
if (reason === 'max_output_tokens') stopReason = 'max_tokens'
|
|
218
|
+
break
|
|
219
|
+
}
|
|
220
|
+
default:
|
|
221
|
+
break
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} catch (err: unknown) {
|
|
225
|
+
if (signal.aborted) return
|
|
226
|
+
yield { type: 'error', message: networkErrorMessage(this.baseUrl, err, 'stream error') }
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (signal.aborted) return
|
|
231
|
+
yield { type: 'done', inputTokens, outputTokens, stopReason }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function pickString(value: unknown): string | undefined {
|
|
236
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function resolveCallId(parsed: Record<string, unknown>, calls: Map<string, StreamingToolCall>): string | undefined {
|
|
240
|
+
return (
|
|
241
|
+
pickString(parsed.call_id)
|
|
242
|
+
?? pickString(parsed.item_id)
|
|
243
|
+
?? pickString((parsed.item as Record<string, unknown> | undefined)?.call_id)
|
|
244
|
+
?? pickString((parsed.item as Record<string, unknown> | undefined)?.id)
|
|
245
|
+
?? (calls.size === 1 ? Array.from(calls.keys())[0] : undefined)
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function readUsage(usage: Record<string, unknown> | undefined): { input?: number; output?: number } {
|
|
250
|
+
if (!usage) return {}
|
|
251
|
+
const input = typeof usage.input_tokens === 'number'
|
|
252
|
+
? usage.input_tokens
|
|
253
|
+
: typeof usage.prompt_tokens === 'number' ? usage.prompt_tokens : undefined
|
|
254
|
+
const output = typeof usage.output_tokens === 'number'
|
|
255
|
+
? usage.output_tokens
|
|
256
|
+
: typeof usage.completion_tokens === 'number' ? usage.completion_tokens : undefined
|
|
257
|
+
return { input, output }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function parseToolArguments(input: string): Record<string, unknown> {
|
|
261
|
+
const trimmed = input.trim()
|
|
262
|
+
if (!trimmed) return {}
|
|
263
|
+
try {
|
|
264
|
+
const parsed = JSON.parse(trimmed) as unknown
|
|
265
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
266
|
+
? (parsed as Record<string, unknown>)
|
|
267
|
+
: {}
|
|
268
|
+
} catch {
|
|
269
|
+
return {}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function networkErrorMessage(baseUrl: string, err: unknown, fallback = 'network error'): string {
|
|
274
|
+
const message = (err as Error).message || fallback
|
|
275
|
+
return `openai request failed at ${baseUrl}: ${message}`
|
|
276
|
+
}
|
|
@@ -6,10 +6,19 @@ 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 { OpenAIResponsesProvider } from './openai-responses.js'
|
|
9
10
|
import { anthropicTools, geminiTools, openAITools } from '../tools/registry.js'
|
|
10
|
-
import { openAIBaseUrlFor } from '../models/catalog.js'
|
|
11
|
+
import { openAIBaseUrlFor, OPENAI_OAUTH_DEFAULT_MODEL, isOpenAIOAuthAllowedModel } from '../models/catalog.js'
|
|
12
|
+
import {
|
|
13
|
+
getOpenAIOAuthCredentials,
|
|
14
|
+
setOpenAIOAuthCredentials,
|
|
15
|
+
type OpenAIOAuthCredentials,
|
|
16
|
+
} from '../auth/openaiOAuth/credentials.js'
|
|
17
|
+
import { refreshOpenAIAccessToken, shouldRefresh } from '../auth/openaiOAuth/refresh.js'
|
|
11
18
|
import type { Tool } from '../tools/contracts.js'
|
|
12
19
|
|
|
20
|
+
export const OPENAI_CHATGPT_BACKEND_URL = 'https://chatgpt.com/backend-api/codex'
|
|
21
|
+
|
|
13
22
|
export function isLocalProvider(provider: string): boolean {
|
|
14
23
|
return provider === 'llamacpp'
|
|
15
24
|
}
|
|
@@ -27,16 +36,84 @@ export function createProvider(config: EthagentConfig, options: { mode?: Session
|
|
|
27
36
|
tools: openAITools(mode, toolContext),
|
|
28
37
|
})
|
|
29
38
|
case 'openai':
|
|
30
|
-
return
|
|
31
|
-
id: 'openai',
|
|
32
|
-
model: config.model,
|
|
33
|
-
baseUrl: openAIBaseUrlFor(config),
|
|
34
|
-
loadApiKey: () => getKey('openai'),
|
|
35
|
-
tools: openAITools(mode, toolContext),
|
|
36
|
-
})
|
|
39
|
+
return createOpenAIProvider(config, openAITools(mode, toolContext))
|
|
37
40
|
case 'anthropic':
|
|
38
41
|
return new AnthropicProvider({ model: config.model, tools: anthropicTools(mode, toolContext) })
|
|
39
42
|
case 'gemini':
|
|
40
43
|
return new GeminiProvider({ model: config.model, tools: geminiTools(mode, toolContext) })
|
|
41
44
|
}
|
|
42
45
|
}
|
|
46
|
+
|
|
47
|
+
function createOpenAIProvider(config: EthagentConfig, tools: ReturnType<typeof openAITools>): Provider {
|
|
48
|
+
return new OpenAIRoutingProvider(config, tools)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class OpenAIRoutingProvider implements Provider {
|
|
52
|
+
readonly id = 'openai' as const
|
|
53
|
+
readonly model: string
|
|
54
|
+
readonly supportsTools: boolean
|
|
55
|
+
private delegate: Provider | null = null
|
|
56
|
+
private readonly config: EthagentConfig
|
|
57
|
+
private readonly tools: ReturnType<typeof openAITools>
|
|
58
|
+
|
|
59
|
+
constructor(config: EthagentConfig, tools: ReturnType<typeof openAITools>) {
|
|
60
|
+
this.config = config
|
|
61
|
+
this.tools = tools
|
|
62
|
+
this.model = config.model
|
|
63
|
+
this.supportsTools = tools.length > 0
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async *complete(...args: Parameters<Provider['complete']>): ReturnType<Provider['complete']> {
|
|
67
|
+
if (!this.delegate) this.delegate = await this.resolveDelegate()
|
|
68
|
+
yield* this.delegate.complete(...args)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private async resolveDelegate(): Promise<Provider> {
|
|
72
|
+
const oauth = await loadFreshOAuthCredentials()
|
|
73
|
+
if (oauth) {
|
|
74
|
+
const oauthModel = isOpenAIOAuthAllowedModel(this.model) ? this.model : OPENAI_OAUTH_DEFAULT_MODEL
|
|
75
|
+
return new OpenAIResponsesProvider({
|
|
76
|
+
model: oauthModel,
|
|
77
|
+
baseUrl: OPENAI_CHATGPT_BACKEND_URL,
|
|
78
|
+
accessToken: oauth.accessToken,
|
|
79
|
+
accountId: oauth.accountId,
|
|
80
|
+
tools: this.tools,
|
|
81
|
+
refresh: async () => {
|
|
82
|
+
const next = await loadFreshOAuthCredentials({ force: true })
|
|
83
|
+
if (!next) throw new Error('No OAuth credentials available')
|
|
84
|
+
return next.accessToken
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
return new OpenAIChatProvider({
|
|
89
|
+
id: 'openai',
|
|
90
|
+
model: this.model,
|
|
91
|
+
baseUrl: openAIBaseUrlFor(this.config),
|
|
92
|
+
loadApiKey: () => getKey('openai'),
|
|
93
|
+
tools: this.tools,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function loadFreshOAuthCredentials(options: { force?: boolean } = {}): Promise<OpenAIOAuthCredentials | null> {
|
|
99
|
+
const current = await getOpenAIOAuthCredentials()
|
|
100
|
+
if (!current) return null
|
|
101
|
+
if (!options.force && !shouldRefresh(current)) return current
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const refreshed = await refreshOpenAIAccessToken(current.refreshToken)
|
|
105
|
+
const now = Date.now()
|
|
106
|
+
const next: OpenAIOAuthCredentials = {
|
|
107
|
+
accessToken: refreshed.accessToken,
|
|
108
|
+
refreshToken: refreshed.refreshToken,
|
|
109
|
+
idToken: refreshed.idToken ?? current.idToken,
|
|
110
|
+
accountId: refreshed.accountId ?? current.accountId,
|
|
111
|
+
expiresAt: now + refreshed.expiresIn * 1000,
|
|
112
|
+
lastRefreshAt: now,
|
|
113
|
+
}
|
|
114
|
+
await setOpenAIOAuthCredentials(next)
|
|
115
|
+
return next
|
|
116
|
+
} catch {
|
|
117
|
+
return current
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -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' ? '
|
|
27
|
+
return mode === 'plan' ? 'plan mode' : mode === 'accept-edits' ? 'accept edits on' : ''
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export function modePolicy(mode: PolicyMode): ModePolicy {
|
|
@@ -76,7 +76,7 @@ function buildToolEnabledPrompt(ctx: SystemPromptContext): string {
|
|
|
76
76
|
'**DIRECT REQUESTS**: If the user asks to change directory, list files, or read a file, respond with exactly one matching native tool call. Do not substitute prose or claim the action was taken.',
|
|
77
77
|
'**EVIDENCE REQUIRED**: Do not claim a path is missing, a directory does not exist, or a file is absent unless you have a `list_directory` or `read_file` result from this conversation that confirms it.',
|
|
78
78
|
'**TOOL TYPING**: Tool names are NOT shell commands. NEVER pass `list_directory`, `read_file`, `edit_file`, or `change_directory` directly to `run_bash`. Call the matching native tool.',
|
|
79
|
-
'
|
|
79
|
+
'**PREFER NATIVE TOOLS**: If a request can be answered by `list_directory`, `read_file`, `edit_file`, `write_file`, `delete_file`, or `change_directory`, use that tool. Treat reaching for `run_bash` as a flag that you may be doing it wrong — only proceed if the action genuinely needs a real shell.',
|
|
80
80
|
...(ctx.mode === 'plan'
|
|
81
81
|
? [
|
|
82
82
|
'Only read/list tools and permission-gated private continuity reads are available in plan mode.',
|
|
@@ -93,11 +93,14 @@ function buildToolEnabledPrompt(ctx: SystemPromptContext): string {
|
|
|
93
93
|
'When exact private continuity text is needed for surgical removal or targeted replacement, call `read_private_continuity_file` with `file: "MEMORY.md"` or `file: "SOUL.md"` first.',
|
|
94
94
|
'When the user wants memory, persona, preferences, or private identity continuity changed, call `propose_private_continuity_edit`; do NOT create, overwrite, or patch SOUL.md/MEMORY.md with `write_file` or `edit_file`.',
|
|
95
95
|
'For private continuity, edit the existing scaffold and build on top of it: prefer `appendToSection`+`appendText` for new notes or use `oldText`+`newText` for targeted replacement. Never omit the edit anchor, never create a new file, and never replace the whole file.',
|
|
96
|
+
'Never call `propose_private_continuity_edit` with `{}` or only `file`. Send exactly one edit mode: either `appendToSection` + non-empty `appendText`, or `oldText` + `newText`. Omit the fields you are not using — do not pass empty strings for `oldText`/`newText` alongside an append, or empty `appendToSection`/`appendText` alongside a targeted edit.',
|
|
96
97
|
'If the user asks to remember preferences or facts, call exactly one private continuity append such as `{"file":"MEMORY.md","appendToSection":"Durable User Preferences","appendText":"- User preference or durable memory."}`.',
|
|
97
98
|
'If the user asks to change persona or standing behavior, call exactly one private continuity append such as `{"file":"SOUL.md","appendToSection":"Persona","appendText":"- Persona or standing behavior."}`.',
|
|
98
99
|
]
|
|
99
100
|
: ['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
101
|
'Use `run_bash` **only** when true shell execution is necessary.',
|
|
102
|
+
'**NO BASH EXPLORATION**: Never use `run_bash` to inspect the workspace. That means no `node -e`, no heredocs (`<<EOF`, `<<\'NODE\'`), no `find`, `grep`, `cat`, `head`, `tail`, `ls`, `dir`, `type`, `tree`, or shell loops to read or walk files. Use `list_directory` for directories and `read_file` for files. `run_bash` is reserved for actions that require a real shell: running tests, builds, git operations, launching processes. If you catch yourself writing a one-liner to read or list something, stop and call the native tool instead.',
|
|
103
|
+
'**DISCOVERY BUDGET**: For exploratory or "tell me about" questions, target ≤5 tool calls total. If 5 calls of the same kind have not given you enough to answer, the question does not need more depth — answer from what you have. Do not recursively scan, walk every subdirectory, or write deeper scripts to be more thorough than the user asked for. Stop and reply.',
|
|
101
104
|
'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.',
|
|
102
105
|
'**CWD CONTINUITY**: The working directory below is authoritative. After `change_directory` succeeds, use the new path as the base for subsequent actions.',
|
|
103
106
|
'Do not lag behind the CWD. Edit/read relative to the *current* working directory.',
|
|
@@ -120,7 +123,6 @@ function buildToolEnabledPrompt(ctx: SystemPromptContext): string {
|
|
|
120
123
|
? [
|
|
121
124
|
'For private SOUL.md or MEMORY.md inspection, do not search project folders. Call `read_private_continuity_file` with `file: "SOUL.md"` or `file: "MEMORY.md"`.',
|
|
122
125
|
'For private SOUL.md or MEMORY.md changes, call `propose_private_continuity_edit` with `file: "SOUL.md"` or `file: "MEMORY.md"` and an in-place append/replacement payload.',
|
|
123
|
-
'Never call `propose_private_continuity_edit` with `{}` or only `file`. For memory/preferences include `appendToSection: "Durable User Preferences"` and a non-empty `appendText`; for persona include `appendToSection: "Persona"` and a non-empty `appendText`.',
|
|
124
126
|
]
|
|
125
127
|
: []),
|
|
126
128
|
'For targeted private continuity edits with `oldText`, copy the text verbatim from the most recent `read_private_continuity_file` output. For workspace targeted edits, copy from the most recent `read_file` output.',
|
|
@@ -17,10 +17,7 @@ import type {
|
|
|
17
17
|
import { setCwd as setRuntimeCwd } from './cwd.js'
|
|
18
18
|
import type { EthagentConfig } from '../storage/config.js'
|
|
19
19
|
import type { SessionMessage } from '../storage/sessions.js'
|
|
20
|
-
import {
|
|
21
|
-
summarizeToolInput,
|
|
22
|
-
toolResultContentForRow,
|
|
23
|
-
} from '../chat/chatScreenUtils.js'
|
|
20
|
+
import { toolResultContentForRow, toolResultDiffForRow } from '../chat/chatScreenUtils.js'
|
|
24
21
|
import type { MessageRow } from '../chat/MessageList.js'
|
|
25
22
|
import { modePolicy, toPermissionMode, type SessionMode } from './sessionMode.js'
|
|
26
23
|
|
|
@@ -239,7 +236,7 @@ export async function runPendingToolUses(args: {
|
|
|
239
236
|
id: rowId,
|
|
240
237
|
name: toolUse.name,
|
|
241
238
|
summary: toolUse.name,
|
|
242
|
-
input:
|
|
239
|
+
input: toolUse.input,
|
|
243
240
|
},
|
|
244
241
|
])
|
|
245
242
|
await args.persistTurnMessage({
|
|
@@ -282,9 +279,15 @@ async function recordToolResult(
|
|
|
282
279
|
): Promise<void> {
|
|
283
280
|
const isError = !result.ok
|
|
284
281
|
const resultContent = toolResultContentForRow(toolUse.name, result.content, isError)
|
|
282
|
+
const diff = toolResultDiffForRow(result.content, isError)
|
|
285
283
|
args.updateRows(prev => prev.map(row =>
|
|
286
284
|
row.role === 'tool_call' && row.id === rowId
|
|
287
|
-
? {
|
|
285
|
+
? {
|
|
286
|
+
...row,
|
|
287
|
+
result: diff
|
|
288
|
+
? { content: resultContent, summary: result.summary, isError, diff }
|
|
289
|
+
: { content: resultContent, summary: result.summary, isError },
|
|
290
|
+
}
|
|
288
291
|
: row,
|
|
289
292
|
))
|
|
290
293
|
await args.persistTurnMessage({
|