@thirdfy/agent-cli 0.4.0 → 0.4.2

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 (64) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +5 -8
  3. package/package.json +3 -3
  4. package/scripts/validate-cli-contract-schemas.mjs +1 -0
  5. package/src/cli/help.mjs +1 -1
  6. package/src/cli/manifest.mjs +8 -1
  7. package/src/cli/options/index.mjs +11 -11
  8. package/src/cli/program.mjs +1 -1
  9. package/src/commands/{agent.mjs → account/agent.mjs} +5 -5
  10. package/src/commands/{credits.mjs → account/credits.mjs} +73 -17
  11. package/src/commands/{wallet.mjs → account/wallet.mjs} +136 -21
  12. package/src/commands/{walletControl.mjs → account/walletControl.mjs} +3 -3
  13. package/src/commands/discovery.mjs +9 -9
  14. package/src/commands/execute.mjs +2 -2
  15. package/src/commands/index.mjs +9 -9
  16. package/src/commands/{bootstrap.mjs → onboarding/bootstrap.mjs} +8 -8
  17. package/src/commands/{consoleMandate.mjs → onboarding/consoleMandate.mjs} +2 -2
  18. package/src/commands/{credentials.mjs → onboarding/credentials.mjs} +6 -6
  19. package/src/commands/{delegation.mjs → onboarding/delegation.mjs} +5 -5
  20. package/src/commands/{avantis.mjs → venues/avantis.mjs} +1 -1
  21. package/src/commands/{condor.mjs → venues/condor.mjs} +4 -4
  22. package/src/commands/{hummingbot.mjs → venues/hummingbot.mjs} +4 -4
  23. package/src/commands/{hyperliquid.mjs → venues/hyperliquid.mjs} +1 -1
  24. package/src/commands/{lighter.mjs → venues/lighter.mjs} +1 -1
  25. package/src/commands/{polymarket.mjs → venues/polymarket.mjs} +4 -4
  26. package/src/commands/{venue.mjs → venues/venue.mjs} +3 -3
  27. package/src/contracts/cli/schemas/wallet-profile.response.schema.json +127 -0
  28. package/src/core/context.mjs +20 -0
  29. package/src/core/earnclawAttribution.mjs +1 -1
  30. package/src/core/{actionTimeouts.mjs → execution/actionTimeouts.mjs} +2 -2
  31. package/src/core/{walletExecuteChainId.mjs → execution/walletExecuteChainId.mjs} +1 -1
  32. package/src/core/http.mjs +1 -1
  33. package/src/runtime/execution/hostedRuntimeWriteGate.mjs +1 -1
  34. package/src/runtime/execution/payloads.mjs +4 -4
  35. package/src/runtime/execution/runners/agentWalletLane.mjs +109 -0
  36. package/src/runtime/execution/runners/createExecutionRunners.mjs +315 -0
  37. package/src/runtime/execution/runners/fallbackHints.mjs +54 -0
  38. package/src/runtime/execution/runners/hybridLane.mjs +88 -0
  39. package/src/runtime/execution/runners/managedPreflight.mjs +253 -0
  40. package/src/runtime/execution/runners/managedResponse.mjs +119 -0
  41. package/src/runtime/execution/runners/selfLane.mjs +232 -0
  42. package/src/runtime/execution/runners/thirdfyLane.mjs +97 -0
  43. package/src/runtime/execution/svmLane.mjs +1 -1
  44. package/src/runtime/handlers.mjs +17 -16
  45. package/src/runtime/main.mjs +1 -1
  46. package/src/runtime/risk/notional.mjs +1 -1
  47. package/src/runtime/validation/actionParamsGate.mjs +1 -1
  48. package/src/runtime/execution/runners.mjs +0 -1134
  49. /package/src/cli/options/{agent.mjs → account/agent.mjs} +0 -0
  50. /package/src/cli/options/{credits.mjs → account/credits.mjs} +0 -0
  51. /package/src/cli/options/{wallet.mjs → account/wallet.mjs} +0 -0
  52. /package/src/cli/options/{walletControl.mjs → account/walletControl.mjs} +0 -0
  53. /package/src/cli/options/{bootstrap.mjs → onboarding/bootstrap.mjs} +0 -0
  54. /package/src/cli/options/{credentials.mjs → onboarding/credentials.mjs} +0 -0
  55. /package/src/cli/options/{delegation.mjs → onboarding/delegation.mjs} +0 -0
  56. /package/src/cli/options/{condor.mjs → venues/condor.mjs} +0 -0
  57. /package/src/cli/options/{hummingbot.mjs → venues/hummingbot.mjs} +0 -0
  58. /package/src/cli/options/{polymarket.mjs → venues/polymarket.mjs} +0 -0
  59. /package/src/cli/options/{venue.mjs → venues/venue.mjs} +0 -0
  60. /package/src/core/{executionContext.mjs → execution/executionContext.mjs} +0 -0
  61. /package/src/core/{fundingTokenIn.mjs → execution/fundingTokenIn.mjs} +0 -0
  62. /package/src/core/{hostedRuntimeIdempotency.mjs → execution/hostedRuntimeIdempotency.mjs} +0 -0
  63. /package/src/core/{readOnlyActions.mjs → execution/readOnlyActions.mjs} +0 -0
  64. /package/src/core/{signedTx.mjs → execution/signedTx.mjs} +0 -0
@@ -0,0 +1,315 @@
1
+ import { ACTION_PREFLIGHT_VENUE_FIELDS, isApiActionBlock } from '../actionPreflight.mjs';
2
+ import { normalizeHybridWalletMode } from '../../../core/runMode.mjs';
3
+ import { apiPost } from '../../../core/http.mjs';
4
+ import { actionNameForApi } from '../../actions/selection.mjs';
5
+ import { isReadOnlyAction } from './managedResponse.mjs';
6
+ import { createFallbackHints } from './fallbackHints.mjs';
7
+ import { createManagedPreflight } from './managedPreflight.mjs';
8
+ import { createThirdfyLane } from './thirdfyLane.mjs';
9
+ import { createSelfLane } from './selfLane.mjs';
10
+ import { createAgentWalletLane } from './agentWalletLane.mjs';
11
+ import { createHybridLane } from './hybridLane.mjs';
12
+ import { createRequire } from 'module';
13
+
14
+ const require = createRequire(import.meta.url);
15
+ const { normalizeIntentResponse } = require('../../../utils/cli/intentNormalization.cjs');
16
+
17
+ /** Execution runners: the preflight and the run for each run mode, wired from the lane files. */
18
+ export function createExecutionRunners(deps) {
19
+ const {
20
+ buildIntentPayload,
21
+ shouldUseBuildTxPreflight,
22
+ } = deps;
23
+ const { applyExecutionFallbackHints } = createFallbackHints(deps);
24
+ const { shouldUseManagedReadFastPath, resolveManagedExecutionPreflight } = createManagedPreflight(deps);
25
+ const { executeThirdfyRun, runReadOnlyActionViaIntentRail } = createThirdfyLane(deps);
26
+ const { executeSelfRun, performSelfBroadcast, resolveEffectCheckResult, submitSignedTx } = createSelfLane(deps, { runReadOnlyActionViaIntentRail });
27
+ const { executeManagedWalletRun } = createAgentWalletLane(deps, { shouldUseManagedReadFastPath, resolveManagedExecutionPreflight, runReadOnlyActionViaIntentRail });
28
+ const { executeHybridRun } = createHybridLane(deps, { runReadOnlyActionViaIntentRail, executeManagedWalletRun, executeSelfRun });
29
+
30
+ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
31
+ const resolvedAction = resolved.resolvedAction;
32
+ if (runMode === 'agent_wallet') {
33
+ const preflight = await resolveManagedExecutionPreflight(ctx, flags, resolved, {
34
+ runMode,
35
+ // Reads carry no transaction; keep funding optional so preflight matches run.
36
+ requireFundingCheck: !isReadOnlyAction(resolved),
37
+ preflightSeed: options.managedPreflight,
38
+ // Ask the API to check policy and venue readiness for this action, not just the wallet.
39
+ evaluateAction: true,
40
+ });
41
+ const normalized = normalizeIntentResponse({
42
+ success: preflight.success,
43
+ status: preflight.success ? 'ready' : 'failed',
44
+ mode: 'agent_wallet',
45
+ executionWalletAddress: preflight.executionWalletAddress || null,
46
+ signerMethod: preflight.signerMethod || 'managed_wallet_server',
47
+ blockedReason: preflight.blockedReason || null,
48
+ blockedStage: preflight.blockedStage || null,
49
+ error: preflight.success ? null : preflight.error || 'Managed wallet preflight failed',
50
+ preflightBlocked: !preflight.success,
51
+ fundingTokenBalance: preflight.fundingTokenBalance || null,
52
+ executionWalletPreflight: preflight,
53
+ });
54
+ normalized.executionWalletAddress = preflight.executionWalletAddress || null;
55
+ normalized.signerMethod = preflight.signerMethod || 'managed_wallet_server';
56
+ normalized.executionWalletPreflight = preflight;
57
+ normalized.fundingTokenBalance = preflight.fundingTokenBalance || null;
58
+ normalized.policyEvaluated = preflight.policyEvaluated === true;
59
+ for (const key of [...ACTION_PREFLIGHT_VENUE_FIELDS, 'executionVenue']) {
60
+ if (preflight[key] != null) normalized[key] = preflight[key];
61
+ }
62
+ // Only an API refusal of the action is `blocked`; CLI-local checks keep `failed`.
63
+ const apiBlocked = isApiActionBlock(preflight);
64
+ if (apiBlocked) normalized.status = 'blocked';
65
+ return {
66
+ message: preflight.success
67
+ ? 'Managed wallet preflight completed'
68
+ : preflight.blockedStage === 'venue_readiness'
69
+ ? 'Managed wallet preflight blocked: venue not ready'
70
+ : apiBlocked
71
+ ? 'Managed wallet preflight blocked by policy'
72
+ : 'Managed wallet preflight failed',
73
+ route: 'execution_wallet_preflight',
74
+ normalized,
75
+ };
76
+ }
77
+ if (runMode === 'self') {
78
+ // Reads have no unsigned tx to prepare. Keep preflight on validation-only execute-intent so
79
+ // operators dry-running self mode do not trigger a real catalog read (run still uses the read rail).
80
+ if (isReadOnlyAction(resolved)) {
81
+ const payload = buildIntentPayload(flags, {
82
+ validationOnly: true,
83
+ forceIdempotency: false,
84
+ resolvedAction,
85
+ apiAction: actionNameForApi(resolved),
86
+ runMode: 'self',
87
+ mirrorOnly: false,
88
+ });
89
+ const response = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', payload);
90
+ const normalized = normalizeIntentResponse(response);
91
+ const blockedCount = normalized.blocked || 0;
92
+ if (!normalized.success || blockedCount > 0) {
93
+ normalized.success = false;
94
+ normalized.error = normalized.error || 'Preflight blocked self execution';
95
+ normalized.preflightBlocked = true;
96
+ }
97
+ return {
98
+ message: normalized.success
99
+ ? 'Self preflight completed'
100
+ : normalized.preflightBlocked
101
+ ? 'Self preflight blocked by governance'
102
+ : 'Self preflight failed',
103
+ route: 'execute_intent_validation',
104
+ normalized,
105
+ };
106
+ }
107
+ const route = shouldUseBuildTxPreflight(runMode, resolved) ? 'build_tx' : 'execute_intent_validation';
108
+ const normalized = await executeSelfRun(
109
+ ctx,
110
+ flags,
111
+ resolved,
112
+ { skipPreflight: route === 'build_tx' }
113
+ );
114
+ const blockedByGovernance = !normalized.success && Boolean(normalized.preflightBlocked);
115
+ return {
116
+ message: blockedByGovernance
117
+ ? 'Self preflight blocked by governance'
118
+ : normalized.success
119
+ ? 'Self preflight completed (unsigned transaction ready)'
120
+ : 'Self preflight failed',
121
+ route,
122
+ normalized,
123
+ };
124
+ }
125
+ if (runMode === 'hybrid') {
126
+ // Reads have nothing to mirror and need no wallet. Keep preflight on validation-only
127
+ // execute-intent so dry-runs do not trigger a real catalog read (run still uses the read rail).
128
+ if (isReadOnlyAction(resolved)) {
129
+ const payload = buildIntentPayload(flags, {
130
+ validationOnly: true,
131
+ forceIdempotency: false,
132
+ resolvedAction,
133
+ apiAction: actionNameForApi(resolved),
134
+ runMode: 'hybrid',
135
+ mirrorOnly: false,
136
+ hybridWalletMode: options.hybridWalletMode,
137
+ });
138
+ const response = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', payload);
139
+ const normalized = normalizeIntentResponse(response);
140
+ const blockedCount = normalized.blocked || 0;
141
+ if (!normalized.success || blockedCount > 0) {
142
+ normalized.success = false;
143
+ normalized.error = normalized.error || 'Preflight blocked hybrid execution';
144
+ normalized.preflightBlocked = true;
145
+ }
146
+ return {
147
+ message: normalized.success
148
+ ? 'Hybrid preflight completed'
149
+ : normalized.preflightBlocked
150
+ ? 'Hybrid preflight blocked by governance'
151
+ : 'Hybrid preflight failed',
152
+ route: 'execute_intent_validation',
153
+ normalized,
154
+ };
155
+ }
156
+ if (options.hybridWalletMode === 'agent_wallet') {
157
+ const managedPayload = buildIntentPayload(flags, {
158
+ validationOnly: true,
159
+ forceIdempotency: false,
160
+ resolvedAction,
161
+ apiAction: actionNameForApi(resolved),
162
+ runMode: 'agent_wallet',
163
+ mirrorOnly: false,
164
+ hybridWalletMode: 'agent_wallet',
165
+ });
166
+ const managedResponse = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', managedPayload);
167
+ const managedNormalized = normalizeIntentResponse(managedResponse);
168
+ const mirrorPayload = buildIntentPayload(flags, {
169
+ validationOnly: true,
170
+ forceIdempotency: false,
171
+ resolvedAction,
172
+ apiAction: actionNameForApi(resolved),
173
+ runMode: 'hybrid',
174
+ mirrorOnly: true,
175
+ hybridWalletMode: 'agent_wallet',
176
+ });
177
+ const mirrorResponse = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', mirrorPayload);
178
+ const mirrorNormalized = normalizeIntentResponse(mirrorResponse);
179
+ return {
180
+ message:
181
+ managedNormalized.success && mirrorNormalized.success
182
+ ? 'Hybrid preflight completed (agent_wallet + thirdfy mirror)'
183
+ : 'Hybrid preflight failed (agent_wallet + thirdfy mirror)',
184
+ route: 'execute_intent_validation_dual',
185
+ normalized: normalizeIntentResponse({
186
+ success: Boolean(managedNormalized.success && mirrorNormalized.success),
187
+ status: mirrorNormalized.status || managedNormalized.status || 'failed',
188
+ mode: 'hybrid',
189
+ hybridWalletMode: 'agent_wallet',
190
+ managed: managedNormalized,
191
+ mirror: mirrorNormalized,
192
+ error: managedNormalized.error || mirrorNormalized.error || null,
193
+ }),
194
+ };
195
+ }
196
+ }
197
+ const payload = buildIntentPayload(flags, {
198
+ validationOnly: true,
199
+ forceIdempotency: false,
200
+ resolvedAction,
201
+ apiAction: actionNameForApi(resolved),
202
+ runMode,
203
+ mirrorOnly: runMode === 'hybrid',
204
+ });
205
+ const response = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', payload);
206
+ const normalized = normalizeIntentResponse(response);
207
+ return {
208
+ message: normalized.success
209
+ ? runMode === 'hybrid'
210
+ ? 'Hybrid mirror preflight completed'
211
+ : 'Preflight completed'
212
+ : runMode === 'hybrid'
213
+ ? 'Hybrid mirror preflight failed'
214
+ : 'Preflight failed',
215
+ route: 'execute_intent_validation',
216
+ normalized,
217
+ };
218
+ }
219
+
220
+ async function runExecutionByMode(runMode, ctx, flags, resolved, options) {
221
+ if (runMode === 'thirdfy') {
222
+ const normalized = await executeThirdfyRun(ctx, flags, resolved, options, runMode);
223
+ const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
224
+ return {
225
+ code: normalized.success ? 'RUN_QUEUED' : preflightBlocked ? 'PREFLIGHT_BLOCKED' : 'RUN_FAILED',
226
+ message: normalized.success
227
+ ? 'Execution intent queued'
228
+ : preflightBlocked
229
+ ? 'Preflight blocked execution'
230
+ : 'Execution failed',
231
+ idempotencyKey: normalized.idempotencyKey || null,
232
+ normalized,
233
+ };
234
+ }
235
+ if (runMode === 'agent_wallet') {
236
+ const normalized = await executeManagedWalletRun(ctx, flags, resolved, options);
237
+ const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
238
+ return {
239
+ code: normalized.success ? 'WALLET_EXECUTED' : preflightBlocked ? 'PREFLIGHT_BLOCKED' : 'RUN_FAILED',
240
+ message: normalized.success
241
+ ? 'Managed wallet execution completed'
242
+ : preflightBlocked
243
+ ? 'Managed wallet preflight blocked execution'
244
+ : 'Managed wallet execution failed',
245
+ idempotencyKey: normalized.idempotencyKey || null,
246
+ normalized,
247
+ };
248
+ }
249
+ if (runMode === 'self') {
250
+ const normalized = await executeSelfRun(ctx, flags, resolved, options);
251
+ const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
252
+ const readOnlySuccess =
253
+ normalized.success && normalized.routeFallback === 'execute_intent_read_only';
254
+ return {
255
+ code: normalized.success
256
+ ? readOnlySuccess
257
+ ? 'SELF_READ_COMPLETED'
258
+ : 'SELF_UNSIGNED_TX_READY'
259
+ : preflightBlocked
260
+ ? 'PREFLIGHT_BLOCKED'
261
+ : 'SELF_EXECUTION_FAILED',
262
+ message: normalized.success
263
+ ? readOnlySuccess
264
+ ? 'Read-only action completed via delegated execute-intent'
265
+ : 'Unsigned transaction prepared for self-custody execution'
266
+ : preflightBlocked
267
+ ? 'Preflight blocked self execution'
268
+ : 'Self-custody execution failed',
269
+ idempotencyKey: null,
270
+ normalized,
271
+ };
272
+ }
273
+ const normalized = await executeHybridRun(ctx, flags, resolved, options);
274
+ const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
275
+ const hybridWalletMode = normalizeHybridWalletMode(
276
+ normalized.hybridWalletMode || options?.hybridWalletMode || flags.hybridWalletMode || 'self',
277
+ );
278
+ const readOnlySuccess =
279
+ normalized.success && normalized.routeFallback === 'execute_intent_read_only';
280
+ return {
281
+ code: normalized.success
282
+ ? readOnlySuccess
283
+ ? 'HYBRID_READ_COMPLETED'
284
+ : 'HYBRID_READY'
285
+ : preflightBlocked
286
+ ? 'PREFLIGHT_BLOCKED'
287
+ : 'HYBRID_FAILED',
288
+ message: normalized.success
289
+ ? readOnlySuccess
290
+ ? 'Read-only action completed via delegated execute-intent'
291
+ : hybridWalletMode === 'agent_wallet'
292
+ ? 'Hybrid execution prepared (agent wallet + mirror preflight)'
293
+ : 'Hybrid execution prepared (self tx + mirror preflight)'
294
+ : preflightBlocked
295
+ ? 'Preflight blocked hybrid execution'
296
+ : 'Hybrid execution failed',
297
+ idempotencyKey: normalized.idempotencyKey || null,
298
+ normalized,
299
+ };
300
+ }
301
+
302
+ return {
303
+ applyExecutionFallbackHints,
304
+ runPreflightByMode,
305
+ runExecutionByMode,
306
+ executeThirdfyRun,
307
+ executeManagedWalletRun,
308
+ executeSelfRun,
309
+ executeHybridRun,
310
+ resolveManagedExecutionPreflight,
311
+ performSelfBroadcast,
312
+ resolveEffectCheckResult,
313
+ submitSignedTx,
314
+ };
315
+ }
@@ -0,0 +1,54 @@
1
+ /** Hints added to a failed execution answer (Bitfinex onboarding and similar next steps). */
2
+ export function createFallbackHints(deps) {
3
+ const {
4
+ buildBitfinexOnboardingCommand,
5
+ inferBitfinexPermissionProfileFromAction,
6
+ } = deps;
7
+
8
+ function applyExecutionFallbackHints(normalized, { flags, runMode, resolvedAction }) {
9
+ if (!normalized || typeof normalized !== 'object') return normalized;
10
+ const next = { ...normalized };
11
+ const reasons = new Set();
12
+ const topBlockedReason = String(next.blockedReason || '').trim();
13
+ if (topBlockedReason) reasons.add(topBlockedReason);
14
+ const blockedByReason = next.blockedByReason && typeof next.blockedByReason === 'object' ? next.blockedByReason : {};
15
+ for (const key of Object.keys(blockedByReason)) {
16
+ if (String(key || '').trim()) reasons.add(String(key).trim());
17
+ }
18
+ const resolvedActionLower = String(resolvedAction || '').trim().toLowerCase();
19
+ const looksBitfinexAction = resolvedActionLower.includes('bitfinex');
20
+ const permissionProfile = inferBitfinexPermissionProfileFromAction(resolvedActionLower);
21
+ const cexCredentialMissingSignals = new Set([
22
+ 'CEX_CREDENTIAL_NOT_FOUND',
23
+ 'BITFINEX_CREDENTIALS_NOT_IN_CONTEXT',
24
+ 'BITFINEX_CREDENTIALS_NOT_IN_THIRDFY_DELEGATION_CONTEXT',
25
+ 'BITFINEX_ENV_CREDENTIALS_NOT_CONFIGURED',
26
+ ]);
27
+ const hasMissingCredentialSignal = Array.from(reasons).some((code) => cexCredentialMissingSignals.has(code));
28
+ if (looksBitfinexAction && hasMissingCredentialSignal) {
29
+ next.onboardingHint = {
30
+ type: 'bitfinex_credentials',
31
+ message:
32
+ 'Bitfinex credentials are missing for this delegated account. Onboard credentials first, then retry the same command.',
33
+ command: buildBitfinexOnboardingCommand(permissionProfile),
34
+ };
35
+ }
36
+ if (
37
+ looksBitfinexAction &&
38
+ runMode !== 'thirdfy' &&
39
+ runMode !== 'agent_wallet' &&
40
+ (topBlockedReason === 'SELF_MODE_REQUIRES_BUILD_TX' || topBlockedReason === 'HYBRID_MODE_REQUIRES_BUILD_TX')
41
+ ) {
42
+ next.onboardingHint = {
43
+ ...(next.onboardingHint || {}),
44
+ runMode: 'thirdfy',
45
+ runModeMessage: 'Bitfinex actions require delegated execute-intent rail. Re-run with --run-mode thirdfy.',
46
+ };
47
+ }
48
+ return next;
49
+ }
50
+
51
+ return {
52
+ applyExecutionFallbackHints,
53
+ };
54
+ }
@@ -0,0 +1,88 @@
1
+ import { normalizeHybridWalletMode } from '../../../core/runMode.mjs';
2
+ import { apiPost } from '../../../core/http.mjs';
3
+ import { actionNameForApi } from '../../actions/selection.mjs';
4
+ import { isReadOnlyAction } from './managedResponse.mjs';
5
+ import { createRequire } from 'module';
6
+
7
+ const require = createRequire(import.meta.url);
8
+ const { normalizeIntentResponse } = require('../../../utils/cli/intentNormalization.cjs');
9
+
10
+ /** The hybrid lane: the managed wallet or self signing, per the hybrid wallet mode. */
11
+ export function createHybridLane(deps, lanes) {
12
+ const {
13
+ buildIntentPayload,
14
+ } = deps;
15
+ const { runReadOnlyActionViaIntentRail, executeManagedWalletRun, executeSelfRun } = lanes;
16
+
17
+ async function executeHybridRun(ctx, flags, resolved, options) {
18
+ const hybridWalletMode = normalizeHybridWalletMode(options?.hybridWalletMode || flags.hybridWalletMode || 'self');
19
+ // Reads have nothing to mirror and need no wallet, whichever hybrid wallet mode is selected.
20
+ // Answering from the read rail also keeps the payload at the top level, instead of nesting it under
21
+ // `managed` where callers that read `results` cannot find it.
22
+ if (isReadOnlyAction(resolved)) {
23
+ const readResult = await runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, null, 'hybrid');
24
+ readResult.hybridWalletMode = hybridWalletMode;
25
+ return readResult;
26
+ }
27
+ if (hybridWalletMode === 'agent_wallet') {
28
+ const managedResult = await executeManagedWalletRun(ctx, { ...flags, runMode: 'agent_wallet' }, resolved, options);
29
+ if (!managedResult.success) {
30
+ return normalizeIntentResponse({
31
+ ...managedResult,
32
+ mode: 'hybrid',
33
+ hybridWalletMode,
34
+ });
35
+ }
36
+ const mirrorPayload = buildIntentPayload(flags, {
37
+ validationOnly: true,
38
+ forceIdempotency: true,
39
+ resolvedAction: resolved.resolvedAction,
40
+ apiAction: actionNameForApi(resolved),
41
+ runMode: 'hybrid',
42
+ mirrorOnly: true,
43
+ hybridWalletMode,
44
+ });
45
+ const mirrorResponse = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', mirrorPayload);
46
+ const mirrorNormalized = normalizeIntentResponse(mirrorResponse);
47
+ return normalizeIntentResponse({
48
+ success: Boolean(managedResult.success && mirrorNormalized.success),
49
+ status: managedResult.status || mirrorNormalized.status || 'queued',
50
+ mode: 'hybrid',
51
+ hybridWalletMode,
52
+ idempotencyKey: managedResult.idempotencyKey || mirrorPayload.idempotencyKey,
53
+ managed: managedResult,
54
+ mirror: mirrorNormalized,
55
+ });
56
+ }
57
+ const selfResult = await executeSelfRun(ctx, flags, resolved, {
58
+ ...(options || {}),
59
+ runMode: 'hybrid',
60
+ blockedReasonCode: 'HYBRID_MODE_REQUIRES_BUILD_TX',
61
+ });
62
+ if (!selfResult.success) return selfResult;
63
+
64
+ const mirrorPayload = buildIntentPayload(flags, {
65
+ validationOnly: true,
66
+ forceIdempotency: true,
67
+ resolvedAction: resolved.resolvedAction,
68
+ apiAction: actionNameForApi(resolved),
69
+ runMode: 'hybrid',
70
+ mirrorOnly: true,
71
+ });
72
+ const mirrorResponse = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', mirrorPayload);
73
+ const mirrorNormalized = normalizeIntentResponse(mirrorResponse);
74
+ return normalizeIntentResponse({
75
+ success: Boolean(selfResult.success && mirrorNormalized.success),
76
+ status: mirrorNormalized.status || 'ready',
77
+ mode: 'hybrid',
78
+ hybridWalletMode,
79
+ idempotencyKey: mirrorPayload.idempotencyKey,
80
+ self: selfResult,
81
+ mirror: mirrorNormalized,
82
+ });
83
+ }
84
+
85
+ return {
86
+ executeHybridRun,
87
+ };
88
+ }