@thirdfy/agent-cli 0.2.34 → 0.2.35
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/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to `@thirdfy/agent-cli` are documented here. The format is b
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.35] - 2026-07-25
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Read-only detection now matches the Thirdfy API action manifest instead of a narrower client-side guess. `fetch_*`, `show_*`, `dogeos_get_*`, `*_info`, `dogeos_barkswap_get_quote`, `dogeos_laika_read_contract`, and `dogeos_laika_token_state` were previously classified as writes, so under `--run-mode agent_wallet` they went to the execute rail and came back as execution envelopes rather than data. A test now pins the two lists together so they cannot drift apart again.
|
|
12
|
+
- `--run-mode self` and `--run-mode hybrid` serve read-only actions from the read rail. Previously a read under `self` either failed with `SELF_MODE_REQUIRES_BUILD_TX` or, when catalog metadata was missing, asked `/api/v1/agent/build-tx` to build a transaction for a market-data query. Under `hybrid` the payload was nested beneath `managed`, where callers reading `results` could not find it, and a mirror failure could mark a successful read as failed.
|
|
13
|
+
- `normalizeIntentResponse` no longer drops top-level `data` and `raw`. It is a whitelist, and read responses carry their payload in `data`, so a caller could receive a well-formed envelope with nothing in it.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Bitfinex reads under `--run-mode hybrid` are served from the delegated rail instead of returning `HYBRID_MODE_REQUIRES_BUILD_TX` with a hint to rerun using `--run-mode thirdfy`. The guard still applies to Bitfinex writes.
|
|
18
|
+
|
|
7
19
|
## [0.2.34] - 2026-07-25
|
|
8
20
|
|
|
9
21
|
### Fixed
|
package/README.md
CHANGED
|
@@ -40,10 +40,11 @@ Run without global install:
|
|
|
40
40
|
npx @thirdfy/agent-cli --help
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
## What's new in v0.2.
|
|
43
|
+
## What's new in v0.2.35
|
|
44
44
|
|
|
45
|
-
- Read-only actions
|
|
46
|
-
-
|
|
45
|
+
- Read-only actions return the provider payload directly in every run mode. Market data reads such as `get_hyperliquid_perps_meta` no longer come back wrapped as an execution intent, and `--run-mode self` no longer refuses them.
|
|
46
|
+
- Read detection now matches the Thirdfy action catalog, so `fetch_*`, `show_*`, `dogeos_get_*`, and `*_info` actions are recognized as reads.
|
|
47
|
+
- Reads are not gated on execution-wallet funding. The execution address is still reported, but an unfunded wallet does not block a read. Routing and identity checks still apply.
|
|
47
48
|
|
|
48
49
|
Older versions: see [CHANGELOG.md](./CHANGELOG.md) and [GitHub Releases](https://github.com/thirdfy/agent-cli/releases).
|
|
49
50
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only action classification.
|
|
3
|
+
*
|
|
4
|
+
* This mirrors `isReadOnlyAction` in the Thirdfy API
|
|
5
|
+
* (`src/services/agents/execution/catalog/actionContractManifest.ts`). Keep the two in sync: when the
|
|
6
|
+
* client and the server disagree about what counts as a read, reads get sent to the managed-wallet
|
|
7
|
+
* execute rail and come back shaped as an execution envelope instead of provider data.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const READ_ONLY_PREFIXES = ['get_', 'list_', 'fetch_', 'search_', 'show_', 'dogeos_get_'];
|
|
11
|
+
|
|
12
|
+
const READ_ONLY_EXACT = new Set([
|
|
13
|
+
'dogeos_barkswap_get_quote',
|
|
14
|
+
'dogeos_laika_read_contract',
|
|
15
|
+
'dogeos_laika_token_state',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export function normalizeActionKey(actionKey) {
|
|
19
|
+
return String(actionKey || '')
|
|
20
|
+
.trim()
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replace(/-/g, '_');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isReadOnlyActionName(actionKey) {
|
|
26
|
+
const key = normalizeActionKey(actionKey);
|
|
27
|
+
if (!key) return false;
|
|
28
|
+
if (READ_ONLY_EXACT.has(key)) return true;
|
|
29
|
+
if (key.endsWith('_info')) return true;
|
|
30
|
+
return READ_ONLY_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Catalog metadata can mark an action read-only even when the name does not match. Production
|
|
35
|
+
* defaults set `supportsExecute: true` on nearly everything, so this rarely fires on its own.
|
|
36
|
+
*/
|
|
37
|
+
export function isReadOnlyActionMeta(meta) {
|
|
38
|
+
if (!meta || typeof meta !== 'object') return false;
|
|
39
|
+
return meta.supportsExecute === false && meta.supportsExecuteIntent !== false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isReadOnlyResolvedAction(resolved, options = {}) {
|
|
43
|
+
const meta = resolved?.resolvedActionMeta;
|
|
44
|
+
if (options.requireMeta && (!meta || typeof meta !== 'object')) return false;
|
|
45
|
+
return isReadOnlyActionName(resolved?.resolvedAction) || isReadOnlyActionMeta(meta);
|
|
46
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '../../core/runMode.mjs';
|
|
12
12
|
import { apiGet, apiPost } from '../../core/http.mjs';
|
|
13
13
|
import { withActionTimeout } from '../../core/actionTimeouts.mjs';
|
|
14
|
+
import { isReadOnlyResolvedAction } from '../../core/readOnlyActions.mjs';
|
|
14
15
|
import { createRequire } from 'module';
|
|
15
16
|
|
|
16
17
|
const require = createRequire(import.meta.url);
|
|
@@ -80,15 +81,7 @@ function applyExecutionFallbackHints(normalized, { flags, runMode, resolvedActio
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
function isReadOnlyAction(resolved) {
|
|
83
|
-
|
|
84
|
-
const action = String(resolved?.resolvedAction || '')
|
|
85
|
-
.trim()
|
|
86
|
-
.toLowerCase()
|
|
87
|
-
.replace(/-/g, '_');
|
|
88
|
-
const readOnlyByName =
|
|
89
|
-
action.startsWith('get_') || action.startsWith('search_') || action.startsWith('list_');
|
|
90
|
-
const readOnlyByMeta = meta.supportsExecute === false && meta.supportsExecuteIntent !== false;
|
|
91
|
-
return readOnlyByName || readOnlyByMeta;
|
|
84
|
+
return isReadOnlyResolvedAction(resolved);
|
|
92
85
|
}
|
|
93
86
|
|
|
94
87
|
// Reads carry no transaction, so wallet balance never blocks them. Every other preflight failure
|
|
@@ -139,6 +132,34 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
|
|
|
139
132
|
};
|
|
140
133
|
}
|
|
141
134
|
if (runMode === 'self') {
|
|
135
|
+
// Reads have no unsigned tx to prepare. Keep preflight on validation-only execute-intent so
|
|
136
|
+
// operators dry-running self mode do not trigger a real catalog read (run still uses the read rail).
|
|
137
|
+
if (isReadOnlyAction(resolved)) {
|
|
138
|
+
const payload = buildIntentPayload(flags, {
|
|
139
|
+
validationOnly: true,
|
|
140
|
+
forceIdempotency: false,
|
|
141
|
+
resolvedAction,
|
|
142
|
+
runMode: 'self',
|
|
143
|
+
mirrorOnly: false,
|
|
144
|
+
});
|
|
145
|
+
const response = await apiPost(ctx, '/api/v1/agent/execute-intent', payload);
|
|
146
|
+
const normalized = normalizeIntentResponse(response);
|
|
147
|
+
const blockedCount = normalized.blocked || 0;
|
|
148
|
+
if (!normalized.success || blockedCount > 0) {
|
|
149
|
+
normalized.success = false;
|
|
150
|
+
normalized.error = normalized.error || 'Preflight blocked self execution';
|
|
151
|
+
normalized.preflightBlocked = true;
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
message: normalized.success
|
|
155
|
+
? 'Self preflight completed'
|
|
156
|
+
: normalized.preflightBlocked
|
|
157
|
+
? 'Self preflight blocked by governance'
|
|
158
|
+
: 'Self preflight failed',
|
|
159
|
+
route: 'execute_intent_validation',
|
|
160
|
+
normalized,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
142
163
|
const route = shouldUseBuildTxPreflight(runMode, resolved) ? 'build_tx' : 'execute_intent_validation';
|
|
143
164
|
const normalized = await executeSelfRun(
|
|
144
165
|
ctx,
|
|
@@ -157,43 +178,74 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
|
|
|
157
178
|
normalized,
|
|
158
179
|
};
|
|
159
180
|
}
|
|
160
|
-
if (runMode === 'hybrid'
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
181
|
+
if (runMode === 'hybrid') {
|
|
182
|
+
// Reads have nothing to mirror and need no wallet. Keep preflight on validation-only
|
|
183
|
+
// execute-intent so dry-runs do not trigger a real catalog read (run still uses the read rail).
|
|
184
|
+
if (isReadOnlyAction(resolved)) {
|
|
185
|
+
const payload = buildIntentPayload(flags, {
|
|
186
|
+
validationOnly: true,
|
|
187
|
+
forceIdempotency: false,
|
|
188
|
+
resolvedAction,
|
|
189
|
+
runMode: 'hybrid',
|
|
190
|
+
mirrorOnly: false,
|
|
191
|
+
hybridWalletMode: options.hybridWalletMode,
|
|
192
|
+
});
|
|
193
|
+
const response = await apiPost(ctx, '/api/v1/agent/execute-intent', payload);
|
|
194
|
+
const normalized = normalizeIntentResponse(response);
|
|
195
|
+
const blockedCount = normalized.blocked || 0;
|
|
196
|
+
if (!normalized.success || blockedCount > 0) {
|
|
197
|
+
normalized.success = false;
|
|
198
|
+
normalized.error = normalized.error || 'Preflight blocked hybrid execution';
|
|
199
|
+
normalized.preflightBlocked = true;
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
message: normalized.success
|
|
203
|
+
? 'Hybrid preflight completed'
|
|
204
|
+
: normalized.preflightBlocked
|
|
205
|
+
? 'Hybrid preflight blocked by governance'
|
|
206
|
+
: 'Hybrid preflight failed',
|
|
207
|
+
route: 'execute_intent_validation',
|
|
208
|
+
normalized,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (options.hybridWalletMode === 'agent_wallet') {
|
|
212
|
+
const managedPayload = buildIntentPayload(flags, {
|
|
213
|
+
validationOnly: true,
|
|
214
|
+
forceIdempotency: false,
|
|
215
|
+
resolvedAction,
|
|
216
|
+
runMode: 'agent_wallet',
|
|
217
|
+
mirrorOnly: false,
|
|
191
218
|
hybridWalletMode: 'agent_wallet',
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
219
|
+
});
|
|
220
|
+
const managedResponse = await apiPost(ctx, '/api/v1/agent/execute-intent', managedPayload);
|
|
221
|
+
const managedNormalized = normalizeIntentResponse(managedResponse);
|
|
222
|
+
const mirrorPayload = buildIntentPayload(flags, {
|
|
223
|
+
validationOnly: true,
|
|
224
|
+
forceIdempotency: false,
|
|
225
|
+
resolvedAction,
|
|
226
|
+
runMode: 'hybrid',
|
|
227
|
+
mirrorOnly: true,
|
|
228
|
+
hybridWalletMode: 'agent_wallet',
|
|
229
|
+
});
|
|
230
|
+
const mirrorResponse = await apiPost(ctx, '/api/v1/agent/execute-intent', mirrorPayload);
|
|
231
|
+
const mirrorNormalized = normalizeIntentResponse(mirrorResponse);
|
|
232
|
+
return {
|
|
233
|
+
message:
|
|
234
|
+
managedNormalized.success && mirrorNormalized.success
|
|
235
|
+
? 'Hybrid preflight completed (agent_wallet + thirdfy mirror)'
|
|
236
|
+
: 'Hybrid preflight failed (agent_wallet + thirdfy mirror)',
|
|
237
|
+
route: 'execute_intent_validation_dual',
|
|
238
|
+
normalized: normalizeIntentResponse({
|
|
239
|
+
success: Boolean(managedNormalized.success && mirrorNormalized.success),
|
|
240
|
+
status: mirrorNormalized.status || managedNormalized.status || 'failed',
|
|
241
|
+
mode: 'hybrid',
|
|
242
|
+
hybridWalletMode: 'agent_wallet',
|
|
243
|
+
managed: managedNormalized,
|
|
244
|
+
mirror: mirrorNormalized,
|
|
245
|
+
error: managedNormalized.error || mirrorNormalized.error || null,
|
|
246
|
+
}),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
197
249
|
}
|
|
198
250
|
const payload = buildIntentPayload(flags, {
|
|
199
251
|
validationOnly: true,
|
|
@@ -249,14 +301,20 @@ async function runExecutionByMode(runMode, ctx, flags, resolved, options) {
|
|
|
249
301
|
if (runMode === 'self') {
|
|
250
302
|
const normalized = await executeSelfRun(ctx, flags, resolved, options);
|
|
251
303
|
const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
|
|
304
|
+
const readOnlySuccess =
|
|
305
|
+
normalized.success && normalized.routeFallback === 'execute_intent_read_only';
|
|
252
306
|
return {
|
|
253
307
|
code: normalized.success
|
|
254
|
-
?
|
|
308
|
+
? readOnlySuccess
|
|
309
|
+
? 'SELF_READ_COMPLETED'
|
|
310
|
+
: 'SELF_UNSIGNED_TX_READY'
|
|
255
311
|
: preflightBlocked
|
|
256
312
|
? 'PREFLIGHT_BLOCKED'
|
|
257
313
|
: 'SELF_EXECUTION_FAILED',
|
|
258
314
|
message: normalized.success
|
|
259
|
-
?
|
|
315
|
+
? readOnlySuccess
|
|
316
|
+
? 'Read-only action completed via delegated execute-intent'
|
|
317
|
+
: 'Unsigned transaction prepared for self-custody execution'
|
|
260
318
|
: preflightBlocked
|
|
261
319
|
? 'Preflight blocked self execution'
|
|
262
320
|
: 'Self-custody execution failed',
|
|
@@ -269,12 +327,22 @@ async function runExecutionByMode(runMode, ctx, flags, resolved, options) {
|
|
|
269
327
|
const hybridWalletMode = normalizeHybridWalletMode(
|
|
270
328
|
normalized.hybridWalletMode || options?.hybridWalletMode || flags.hybridWalletMode || 'self',
|
|
271
329
|
);
|
|
330
|
+
const readOnlySuccess =
|
|
331
|
+
normalized.success && normalized.routeFallback === 'execute_intent_read_only';
|
|
272
332
|
return {
|
|
273
|
-
code: normalized.success
|
|
333
|
+
code: normalized.success
|
|
334
|
+
? readOnlySuccess
|
|
335
|
+
? 'HYBRID_READ_COMPLETED'
|
|
336
|
+
: 'HYBRID_READY'
|
|
337
|
+
: preflightBlocked
|
|
338
|
+
? 'PREFLIGHT_BLOCKED'
|
|
339
|
+
: 'HYBRID_FAILED',
|
|
274
340
|
message: normalized.success
|
|
275
|
-
?
|
|
276
|
-
? '
|
|
277
|
-
:
|
|
341
|
+
? readOnlySuccess
|
|
342
|
+
? 'Read-only action completed via delegated execute-intent'
|
|
343
|
+
: hybridWalletMode === 'agent_wallet'
|
|
344
|
+
? 'Hybrid execution prepared (agent wallet + mirror preflight)'
|
|
345
|
+
: 'Hybrid execution prepared (self tx + mirror preflight)'
|
|
278
346
|
: preflightBlocked
|
|
279
347
|
? 'Preflight blocked hybrid execution'
|
|
280
348
|
: 'Hybrid execution failed',
|
|
@@ -319,7 +387,7 @@ async function executeThirdfyRun(ctx, flags, resolved, options, runMode = 'third
|
|
|
319
387
|
return normalized;
|
|
320
388
|
}
|
|
321
389
|
|
|
322
|
-
async function runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, preflight) {
|
|
390
|
+
async function runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, preflight, reportedMode = 'agent_wallet') {
|
|
323
391
|
const userDid = resolveEffectiveUserDid(flags, {
|
|
324
392
|
runMode: 'agent_wallet',
|
|
325
393
|
hybridWalletMode: options?.hybridWalletMode,
|
|
@@ -338,7 +406,7 @@ async function runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, pre
|
|
|
338
406
|
{ ...options, skipPreflight: true },
|
|
339
407
|
'thirdfy'
|
|
340
408
|
);
|
|
341
|
-
intentResult.mode =
|
|
409
|
+
intentResult.mode = reportedMode;
|
|
342
410
|
intentResult.routeFallback = 'execute_intent_read_only';
|
|
343
411
|
if (preflight) {
|
|
344
412
|
intentResult.executionWalletPreflight = preflight;
|
|
@@ -449,6 +517,12 @@ async function executeManagedWalletRun(ctx, flags, resolved, options) {
|
|
|
449
517
|
async function executeSelfRun(ctx, flags, resolved, options) {
|
|
450
518
|
const skipPreflight = Boolean(options?.skipPreflight);
|
|
451
519
|
const blockedReasonCode = String(options?.blockedReasonCode || 'SELF_MODE_REQUIRES_BUILD_TX');
|
|
520
|
+
// A read has no transaction to build or sign, so the self lane has nothing to do with it. Without
|
|
521
|
+
// this it either fails with SELF_MODE_REQUIRES_BUILD_TX or, when catalog metadata is missing, asks
|
|
522
|
+
// /build-tx to produce a transaction for a market-data query.
|
|
523
|
+
if (isReadOnlyAction(resolved)) {
|
|
524
|
+
return runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, null, options?.runMode || 'self');
|
|
525
|
+
}
|
|
452
526
|
if (!skipPreflight) {
|
|
453
527
|
const preflightPayload = buildIntentPayload(flags, {
|
|
454
528
|
validationOnly: true,
|
|
@@ -491,6 +565,14 @@ async function executeSelfRun(ctx, flags, resolved, options) {
|
|
|
491
565
|
|
|
492
566
|
async function executeHybridRun(ctx, flags, resolved, options) {
|
|
493
567
|
const hybridWalletMode = normalizeHybridWalletMode(options?.hybridWalletMode || flags.hybridWalletMode || 'self');
|
|
568
|
+
// Reads have nothing to mirror and need no wallet, whichever hybrid wallet mode is selected.
|
|
569
|
+
// Answering from the read rail also keeps the payload at the top level, instead of nesting it under
|
|
570
|
+
// `managed` where callers that read `results` cannot find it.
|
|
571
|
+
if (isReadOnlyAction(resolved)) {
|
|
572
|
+
const readResult = await runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, null, 'hybrid');
|
|
573
|
+
readResult.hybridWalletMode = hybridWalletMode;
|
|
574
|
+
return readResult;
|
|
575
|
+
}
|
|
494
576
|
if (hybridWalletMode === 'agent_wallet') {
|
|
495
577
|
const managedResult = await executeManagedWalletRun(ctx, { ...flags, runMode: 'agent_wallet' }, resolved, options);
|
|
496
578
|
if (!managedResult.success) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createCliError } from '../../core/envelope.mjs';
|
|
2
|
+
import { isReadOnlyResolvedAction } from '../../core/readOnlyActions.mjs';
|
|
2
3
|
import {
|
|
3
4
|
sanitizeParamsForSchemaValidation,
|
|
4
5
|
validateParamsSchema,
|
|
@@ -12,15 +13,7 @@ export function resolveActionParamsSchema(resolved) {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export function isReadOnlyCatalogAction(resolved) {
|
|
15
|
-
|
|
16
|
-
if (!meta || typeof meta !== 'object') return false;
|
|
17
|
-
const action = String(resolved?.resolvedAction || '')
|
|
18
|
-
.trim()
|
|
19
|
-
.toLowerCase()
|
|
20
|
-
.replace(/-/g, '_');
|
|
21
|
-
const readOnlyByName = action.startsWith('get_') || action.startsWith('list_') || action.startsWith('search_');
|
|
22
|
-
const readOnlyByMeta = meta.supportsExecute === false && meta.supportsExecuteIntent !== false;
|
|
23
|
-
return readOnlyByName || readOnlyByMeta;
|
|
16
|
+
return isReadOnlyResolvedAction(resolved, { requireMeta: true });
|
|
24
17
|
}
|
|
25
18
|
|
|
26
19
|
export function validateResolvedActionParams(resolved, params) {
|
|
@@ -34,7 +34,14 @@ function normalizeIntentResponse(response) {
|
|
|
34
34
|
const topLevelDelegationSignal = String(response?.delegationSignal || '').trim() || null;
|
|
35
35
|
const policyEvaluated = response?.policyEvaluated === true;
|
|
36
36
|
const preflightBlocked = response?.preflightBlocked === true;
|
|
37
|
+
// This function is a whitelist, so any provider payload outside the known keys is dropped. Read
|
|
38
|
+
// actions return their data in `data` (and sometimes `raw`) rather than in `results`, so those two
|
|
39
|
+
// must survive or the caller gets a well-formed envelope with nothing in it.
|
|
40
|
+
const passthrough = {};
|
|
41
|
+
if (response?.data !== undefined) passthrough.data = response.data;
|
|
42
|
+
if (response?.raw !== undefined) passthrough.raw = response.raw;
|
|
37
43
|
return {
|
|
44
|
+
...passthrough,
|
|
38
45
|
success,
|
|
39
46
|
status: response?.status || (success ? 'queued' : 'failed'),
|
|
40
47
|
intentId: response?.intentId || null,
|