shopping-cli 3.0.0__py3-none-any.whl

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 (82) hide show
  1. shopping_cli/__init__.py +3 -0
  2. shopping_cli/agents/__init__.py +1 -0
  3. shopping_cli/agents/agent_cli.py +31 -0
  4. shopping_cli/agents/buyer_cli.py +163 -0
  5. shopping_cli/agents/merchant_agent.py +263 -0
  6. shopping_cli/agents/merchant_daemon.py +872 -0
  7. shopping_cli/agents/tools.py +488 -0
  8. shopping_cli/api/__init__.py +1 -0
  9. shopping_cli/api/app.py +1353 -0
  10. shopping_cli/api/auth.py +125 -0
  11. shopping_cli/api/fallback_asgi.py +219 -0
  12. shopping_cli/api/handlers/__init__.py +1 -0
  13. shopping_cli/api/handlers/agents.py +277 -0
  14. shopping_cli/api/handlers/audit.py +97 -0
  15. shopping_cli/api/handlers/buyer.py +291 -0
  16. shopping_cli/api/handlers/catalog.py +392 -0
  17. shopping_cli/api/handlers/common.py +76 -0
  18. shopping_cli/api/handlers/conversations.py +172 -0
  19. shopping_cli/api/handlers/erp.py +127 -0
  20. shopping_cli/api/handlers/human_review.py +293 -0
  21. shopping_cli/api/handlers/listings_projection.py +42 -0
  22. shopping_cli/api/handlers/negotiation.py +121 -0
  23. shopping_cli/api/idempotency.py +291 -0
  24. shopping_cli/api/limits.py +59 -0
  25. shopping_cli/api/route_registry.py +105 -0
  26. shopping_cli/api/routes_agents.py +13 -0
  27. shopping_cli/api/routes_conversations.py +13 -0
  28. shopping_cli/api/routes_marketplace.py +13 -0
  29. shopping_cli/api/routes_merchants.py +13 -0
  30. shopping_cli/api/server.py +31 -0
  31. shopping_cli/cli.py +897 -0
  32. shopping_cli/cli_agent_commands.py +386 -0
  33. shopping_cli/cli_buyer_commands.py +231 -0
  34. shopping_cli/cli_catalog_commands.py +281 -0
  35. shopping_cli/cli_common.py +135 -0
  36. shopping_cli/cli_conversation_commands.py +167 -0
  37. shopping_cli/cli_erp_commands.py +60 -0
  38. shopping_cli/cli_listing_commands.py +35 -0
  39. shopping_cli/commerce/__init__.py +6 -0
  40. shopping_cli/commerce/adapters.py +310 -0
  41. shopping_cli/commerce/data_source.py +95 -0
  42. shopping_cli/config.py +262 -0
  43. shopping_cli/contracts/shopping.negotiation/0.1/capabilities.schema.json +47 -0
  44. shopping_cli/contracts/shopping.negotiation/0.1/decision.schema.json +120 -0
  45. shopping_cli/contracts/shopping.negotiation/0.1/policy-result.schema.json +52 -0
  46. shopping_cli/contracts/shopping.negotiation/0.1/snapshot.schema.json +172 -0
  47. shopping_cli/core/__init__.py +1 -0
  48. shopping_cli/core/catalog.py +1281 -0
  49. shopping_cli/core/channels.py +329 -0
  50. shopping_cli/core/conversations.py +502 -0
  51. shopping_cli/core/delivery.py +7 -0
  52. shopping_cli/core/errors.py +43 -0
  53. shopping_cli/core/harness.py +345 -0
  54. shopping_cli/core/limits.py +137 -0
  55. shopping_cli/core/negotiation.py +273 -0
  56. shopping_cli/core/policies.py +356 -0
  57. shopping_cli/core/risk.py +101 -0
  58. shopping_cli/core/tokens.py +33 -0
  59. shopping_cli/data_sources/__init__.py +12 -0
  60. shopping_cli/data_sources/erp_source.py +440 -0
  61. shopping_cli/db/__init__.py +1 -0
  62. shopping_cli/db/migrations.py +415 -0
  63. shopping_cli/db/models.py +325 -0
  64. shopping_cli/db/provenance.py +21 -0
  65. shopping_cli/db/session.py +108 -0
  66. shopping_cli/http_client.py +149 -0
  67. shopping_cli/listings/__init__.py +4 -0
  68. shopping_cli/listings/projection.py +148 -0
  69. shopping_cli/services/__init__.py +1 -0
  70. shopping_cli/services/agents.py +244 -0
  71. shopping_cli/services/audit.py +34 -0
  72. shopping_cli/services/buyer_bootstrap.py +23 -0
  73. shopping_cli/services/conversations.py +209 -0
  74. shopping_cli/services/human_review.py +165 -0
  75. shopping_cli/services/negotiation.py +929 -0
  76. shopping_cli/services/tokens.py +318 -0
  77. shopping_cli-3.0.0.dist-info/METADATA +219 -0
  78. shopping_cli-3.0.0.dist-info/RECORD +82 -0
  79. shopping_cli-3.0.0.dist-info/WHEEL +5 -0
  80. shopping_cli-3.0.0.dist-info/entry_points.txt +4 -0
  81. shopping_cli-3.0.0.dist-info/licenses/LICENSE +201 -0
  82. shopping_cli-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,488 @@
1
+ """Typed marketplace tools used by resident merchant agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import sqlite3
7
+ import urllib.parse
8
+ from typing import Any, Protocol
9
+
10
+ from shopping_cli import VERSION
11
+ from shopping_cli.core.catalog import product_summary, require_merchant
12
+ from shopping_cli.core.conversations import add_flag, append_message, waiting_merchant_conversations
13
+ from shopping_cli.core.errors import ValidationError
14
+ from shopping_cli.core.harness import abandon_agent_message, abandon_stale_agent_messages, claim_agent_message, complete_agent_message, fail_agent_message
15
+ from shopping_cli.db.session import encode_json, now_iso
16
+ from shopping_cli.http_client import MarketplaceHTTPClient, MarketplaceHTTPError
17
+
18
+ DEFAULT_CAPABILITIES = ["catalog", "inventory", "delivery", "consultation"]
19
+ AGENT_STATUSES = {"online", "away", "human_required"}
20
+ MAX_SQLITE_INTEGER = 2**63 - 1
21
+
22
+
23
+ def _non_negative_whole_int(value: Any, field_name: str, default: int = 0) -> int:
24
+ if value in (None, ""):
25
+ return default
26
+ if isinstance(value, bool):
27
+ raise ValueError(f"{field_name} must be a whole number")
28
+ if isinstance(value, int):
29
+ number = value
30
+ elif isinstance(value, float):
31
+ if not math.isfinite(value) or not value.is_integer():
32
+ raise ValueError(f"{field_name} must be a whole number")
33
+ number = int(value)
34
+ else:
35
+ try:
36
+ number = int(str(value).strip())
37
+ except ValueError as exc:
38
+ raise ValueError(f"{field_name} must be a whole number") from exc
39
+ if number < 0:
40
+ raise ValueError(f"{field_name} must be non-negative")
41
+ if number > MAX_SQLITE_INTEGER:
42
+ raise ValueError(f"{field_name} must be <= {MAX_SQLITE_INTEGER}")
43
+ return number
44
+
45
+
46
+ def _positive_whole_int(value: Any, field_name: str) -> int:
47
+ number = _non_negative_whole_int(value, field_name)
48
+ if number <= 0:
49
+ raise ValueError(f"{field_name} must be greater than 0")
50
+ return number
51
+
52
+
53
+ def _normalize_agent_status(value: Any) -> str:
54
+ status = str(value or "").strip() or "online"
55
+ if status not in AGENT_STATUSES:
56
+ raise ValidationError(f"Unknown agent status: {status}")
57
+ return status
58
+
59
+
60
+ def _normalize_capabilities(value: Any) -> list[str]:
61
+ if value is None:
62
+ return list(DEFAULT_CAPABILITIES)
63
+ if not isinstance(value, list):
64
+ raise ValidationError("agent capabilities must be a list of strings")
65
+ normalized: list[str] = []
66
+ for item in value:
67
+ if not isinstance(item, str):
68
+ raise ValidationError("agent capabilities must be a list of strings")
69
+ text = item.strip()
70
+ if text:
71
+ normalized.append(text)
72
+ return normalized or list(DEFAULT_CAPABILITIES)
73
+
74
+
75
+ class MerchantAgentTools(Protocol):
76
+ def heartbeat(
77
+ self,
78
+ merchant_id: str,
79
+ status: str = "online",
80
+ last_error: str = "",
81
+ checked_count: int = 0,
82
+ replied_count: int = 0,
83
+ ) -> dict[str, Any]:
84
+ ...
85
+
86
+ def waiting_merchant_conversations(self, merchant_id: str) -> list[dict[str, Any]]:
87
+ ...
88
+
89
+ def product_summary(self, sku: str) -> dict[str, Any]:
90
+ ...
91
+
92
+ def append_message(
93
+ self,
94
+ conversation_id: str,
95
+ sender: str,
96
+ intent: str,
97
+ text: str,
98
+ structured_payload: dict[str, Any],
99
+ status: str,
100
+ ) -> dict[str, Any]:
101
+ ...
102
+
103
+ def add_flag(self, conversation_id: str, reason: str, sku: str = "") -> dict[str, Any]:
104
+ ...
105
+
106
+ def claim_message(self, agent_id: str, conversation_id: str, message_id: int, idempotency_key: str) -> dict[str, Any]:
107
+ ...
108
+
109
+ def complete_message(self, agent_id: str, message_id: int) -> dict[str, Any]:
110
+ ...
111
+
112
+ def fail_message(self, agent_id: str, message_id: int, error: str) -> dict[str, Any]:
113
+ ...
114
+
115
+ def abandon_message(self, agent_id: str, message_id: int, error: str) -> dict[str, Any]:
116
+ ...
117
+
118
+ def abandon_stale_messages(self, agent_id: str, stale_after_seconds: int = 300) -> list[dict[str, Any]]:
119
+ ...
120
+
121
+
122
+ def record_heartbeat(
123
+ conn: sqlite3.Connection,
124
+ merchant_id: str,
125
+ status: str = "online",
126
+ capabilities: list[str] | None = None,
127
+ pid: int = 0,
128
+ version: str = VERSION,
129
+ last_error: str = "",
130
+ checked_count: int = 0,
131
+ replied_count: int = 0,
132
+ ) -> dict[str, Any]:
133
+ merchant_id = str(merchant_id or "").strip()
134
+ status = _normalize_agent_status(status)
135
+ pid = _non_negative_whole_int(pid, "pid")
136
+ checked_count = _non_negative_whole_int(checked_count, "checked_count")
137
+ replied_count = _non_negative_whole_int(replied_count, "replied_count")
138
+ capabilities = _normalize_capabilities(capabilities)
139
+ require_merchant(conn, merchant_id)
140
+ agent_id = f"shopping-cli-merchant-agent:{merchant_id}"
141
+ now = now_iso()
142
+ conn.execute(
143
+ """
144
+ insert into agents(
145
+ id, type, owner_id, status, capabilities_json, last_seen_at,
146
+ pid, version, last_error, checked_count, replied_count
147
+ )
148
+ values (?, 'merchant', ?, ?, ?, ?, ?, ?, ?, ?, ?)
149
+ on conflict(id) do update set
150
+ status = excluded.status,
151
+ capabilities_json = excluded.capabilities_json,
152
+ last_seen_at = excluded.last_seen_at,
153
+ pid = excluded.pid,
154
+ version = excluded.version,
155
+ last_error = excluded.last_error,
156
+ checked_count = excluded.checked_count,
157
+ replied_count = excluded.replied_count
158
+ """,
159
+ (
160
+ agent_id,
161
+ merchant_id,
162
+ status,
163
+ encode_json(capabilities),
164
+ now,
165
+ pid,
166
+ version or VERSION,
167
+ last_error or "",
168
+ checked_count,
169
+ replied_count,
170
+ ),
171
+ )
172
+ return {
173
+ "id": agent_id,
174
+ "type": "merchant",
175
+ "owner_id": merchant_id,
176
+ "status": status,
177
+ "capabilities": capabilities,
178
+ "last_seen_at": now,
179
+ "pid": pid,
180
+ "version": version or VERSION,
181
+ "last_error": last_error or "",
182
+ "checked_count": checked_count,
183
+ "replied_count": replied_count,
184
+ }
185
+
186
+
187
+ class SQLiteMerchantAgentTools:
188
+ def __init__(self, conn: sqlite3.Connection):
189
+ self.conn = conn
190
+
191
+ def heartbeat(
192
+ self,
193
+ merchant_id: str,
194
+ status: str = "online",
195
+ last_error: str = "",
196
+ checked_count: int = 0,
197
+ replied_count: int = 0,
198
+ ) -> dict[str, Any]:
199
+ return record_heartbeat(
200
+ self.conn,
201
+ merchant_id,
202
+ status=status,
203
+ last_error=last_error,
204
+ checked_count=checked_count,
205
+ replied_count=replied_count,
206
+ )
207
+
208
+ def waiting_merchant_conversations(self, merchant_id: str) -> list[dict[str, Any]]:
209
+ return waiting_merchant_conversations(self.conn, merchant_id)
210
+
211
+ def product_summary(self, sku: str) -> dict[str, Any]:
212
+ return product_summary(self.conn, sku)
213
+
214
+ def append_message(
215
+ self,
216
+ conversation_id: str,
217
+ sender: str,
218
+ intent: str,
219
+ text: str,
220
+ structured_payload: dict[str, Any],
221
+ status: str,
222
+ ) -> dict[str, Any]:
223
+ return append_message(
224
+ self.conn,
225
+ conversation_id,
226
+ sender,
227
+ intent,
228
+ text,
229
+ structured_payload=structured_payload,
230
+ status=status,
231
+ )
232
+
233
+ def add_flag(self, conversation_id: str, reason: str, sku: str = "") -> dict[str, Any]:
234
+ return add_flag(self.conn, conversation_id, reason, sku=sku)
235
+
236
+ def claim_message(self, agent_id: str, conversation_id: str, message_id: int, idempotency_key: str) -> dict[str, Any]:
237
+ return claim_agent_message(self.conn, agent_id, conversation_id, message_id, idempotency_key)
238
+
239
+ def complete_message(self, agent_id: str, message_id: int) -> dict[str, Any]:
240
+ return complete_agent_message(self.conn, agent_id, message_id)
241
+
242
+ def fail_message(self, agent_id: str, message_id: int, error: str) -> dict[str, Any]:
243
+ return fail_agent_message(self.conn, agent_id, message_id, error)
244
+
245
+ def abandon_message(self, agent_id: str, message_id: int, error: str) -> dict[str, Any]:
246
+ return abandon_agent_message(self.conn, agent_id, message_id, error)
247
+
248
+ def abandon_stale_messages(self, agent_id: str, stale_after_seconds: int = 300) -> list[dict[str, Any]]:
249
+ return abandon_stale_agent_messages(self.conn, agent_id, stale_after_seconds=stale_after_seconds)
250
+
251
+
252
+ HTTPMarketplaceError = MarketplaceHTTPError
253
+
254
+
255
+ class HTTPMerchantAgentTools:
256
+ def __init__(
257
+ self,
258
+ base_url: str,
259
+ merchant_id: str,
260
+ merchant_token: str,
261
+ timeout: float = 10.0,
262
+ opener: Any | None = None,
263
+ host: str = "",
264
+ session_id: str = "",
265
+ allow_insecure_http: bool = False,
266
+ ):
267
+ self.merchant_id = str(merchant_id or "").strip()
268
+ if not self.merchant_id:
269
+ raise ValueError("merchant_id is required")
270
+ self.merchant_token = str(merchant_token or "").strip()
271
+ if not self.merchant_token:
272
+ raise ValueError("merchant_token is required")
273
+ self.http = MarketplaceHTTPClient(
274
+ base_url,
275
+ self.merchant_token,
276
+ timeout=timeout,
277
+ opener=opener,
278
+ allow_insecure_http=allow_insecure_http,
279
+ )
280
+ self.base_url = self.http.base_url
281
+ self.timeout = self.http.timeout
282
+ self.host = str(host or "")
283
+ self.session_id = str(session_id or "")
284
+ self._message_created_review_flags: dict[tuple[str, str], dict[str, Any]] = {}
285
+
286
+ def _merchant_payload(self, payload: dict[str, Any] | None = None) -> dict[str, Any]:
287
+ merged = dict(payload or {})
288
+ merged["merchant_id"] = self.merchant_id
289
+ merged["merchant_token"] = self.merchant_token
290
+ return merged
291
+
292
+ def _token_payload(self, payload: dict[str, Any] | None = None) -> dict[str, Any]:
293
+ merged = dict(payload or {})
294
+ merged["merchant_token"] = self.merchant_token
295
+ return merged
296
+
297
+ def _check_merchant(self, merchant_id: str) -> None:
298
+ if merchant_id != self.merchant_id:
299
+ raise ValueError(f"HTTP tools are scoped to merchant {self.merchant_id}, not {merchant_id}")
300
+
301
+ def _should_record_tool_calls(self) -> bool:
302
+ return bool(self.host or self.session_id)
303
+
304
+ def _record_tool_call(
305
+ self,
306
+ tool: str,
307
+ conversation_id: str = "",
308
+ status: str = "ok",
309
+ error: str = "",
310
+ ) -> None:
311
+ if not self._should_record_tool_calls():
312
+ return
313
+ payload = self._token_payload(
314
+ {
315
+ "merchant_id": self.merchant_id,
316
+ "tool": tool,
317
+ "status": status,
318
+ "host": self.host,
319
+ "session_id": self.session_id,
320
+ "actor": f"shopping-cli-merchant-agent:{self.merchant_id}",
321
+ "source_id": f"shopping-cli-merchant-agent:{self.merchant_id}",
322
+ "token_scope": "merchant_agent",
323
+ "error": error,
324
+ }
325
+ )
326
+ if conversation_id:
327
+ payload["conversation_id"] = conversation_id
328
+ try:
329
+ self._request("POST", "/audit/tool-calls", payload)
330
+ except HTTPMarketplaceError:
331
+ return
332
+
333
+ def _request(
334
+ self,
335
+ method: str,
336
+ path: str,
337
+ payload: dict[str, Any] | None = None,
338
+ query: dict[str, Any] | None = None,
339
+ ) -> dict[str, Any]:
340
+ return self.http.request(method, path, payload, query)
341
+
342
+ @staticmethod
343
+ def _response_object(result: dict[str, Any], key: str) -> dict[str, Any]:
344
+ return MarketplaceHTTPClient.response_object(result, key)
345
+
346
+ @staticmethod
347
+ def _response_list(result: dict[str, Any], key: str) -> list[Any]:
348
+ return MarketplaceHTTPClient.response_list(result, key)
349
+
350
+ def heartbeat(
351
+ self,
352
+ merchant_id: str,
353
+ status: str = "online",
354
+ last_error: str = "",
355
+ checked_count: int = 0,
356
+ replied_count: int = 0,
357
+ ) -> dict[str, Any]:
358
+ self._check_merchant(merchant_id)
359
+ result = self._request(
360
+ "POST",
361
+ "/agents/heartbeat",
362
+ self._merchant_payload(
363
+ {
364
+ "status": status,
365
+ "last_error": last_error,
366
+ "checked_count": _non_negative_whole_int(checked_count, "checked_count"),
367
+ "replied_count": _non_negative_whole_int(replied_count, "replied_count"),
368
+ }
369
+ ),
370
+ )
371
+ self._record_tool_call("agent_heartbeat")
372
+ return self._response_object(result, "agent")
373
+
374
+ def waiting_merchant_conversations(self, merchant_id: str) -> list[dict[str, Any]]:
375
+ self._check_merchant(merchant_id)
376
+ path = f"/merchants/{urllib.parse.quote(merchant_id, safe='')}/conversations"
377
+ result = self._request("GET", path, query={"status": "waiting_merchant", "include": "details", "limit": 100})
378
+ return self._response_list(result, "conversations")
379
+
380
+ def product_summary(self, sku: str) -> dict[str, Any]:
381
+ result = self._request("GET", f"/products/{urllib.parse.quote(str(sku), safe='')}")
382
+ return self._response_object(result, "product")
383
+
384
+ def append_message(
385
+ self,
386
+ conversation_id: str,
387
+ sender: str,
388
+ intent: str,
389
+ text: str,
390
+ structured_payload: dict[str, Any],
391
+ status: str,
392
+ ) -> dict[str, Any]:
393
+ result = self._request(
394
+ "POST",
395
+ f"/conversations/{urllib.parse.quote(conversation_id, safe='')}/messages",
396
+ self._token_payload(
397
+ {
398
+ "sender": sender,
399
+ "intent": intent,
400
+ "text": text,
401
+ "structured_payload": structured_payload,
402
+ "status": status,
403
+ }
404
+ ),
405
+ )
406
+ self._record_tool_call("conversation_message", conversation_id=conversation_id)
407
+ conversation = result.get("conversation")
408
+ if isinstance(conversation, dict):
409
+ for flag in conversation.get("flags") or []:
410
+ if isinstance(flag, dict) and flag.get("reason") and not flag.get("resolved_at"):
411
+ self._message_created_review_flags[(conversation_id, str(flag["reason"]))] = dict(flag)
412
+ return self._response_object(result, "message")
413
+
414
+ def add_flag(self, conversation_id: str, reason: str, sku: str = "") -> dict[str, Any]:
415
+ cached = self._message_created_review_flags.pop((conversation_id, reason), None)
416
+ if cached is not None:
417
+ self._record_tool_call("human_review_flag", conversation_id=conversation_id)
418
+ return cached
419
+ result = self._request(
420
+ "POST",
421
+ f"/conversations/{urllib.parse.quote(conversation_id, safe='')}/human-review",
422
+ self._merchant_payload({"reason": reason, "sku": sku, "source_id": f"shopping-cli-merchant-agent:{self.merchant_id}"}),
423
+ )
424
+ self._record_tool_call("human_review_flag", conversation_id=conversation_id)
425
+ return self._response_object(result, "review")
426
+
427
+ def claim_message(self, agent_id: str, conversation_id: str, message_id: int, idempotency_key: str) -> dict[str, Any]:
428
+ result = self._request(
429
+ "POST",
430
+ "/agents/messages/claim",
431
+ self._merchant_payload(
432
+ {
433
+ "agent_id": agent_id,
434
+ "conversation_id": conversation_id,
435
+ "message_id": _positive_whole_int(message_id, "message_id"),
436
+ "idempotency_key": idempotency_key,
437
+ }
438
+ ),
439
+ )
440
+ self._record_tool_call("agent_message_claim", conversation_id=conversation_id)
441
+ return self._response_object(result, "claim")
442
+
443
+ def complete_message(self, agent_id: str, message_id: int) -> dict[str, Any]:
444
+ result = self._request(
445
+ "POST",
446
+ "/agents/messages/complete",
447
+ self._merchant_payload(
448
+ {"agent_id": agent_id, "message_id": _positive_whole_int(message_id, "message_id")}
449
+ ),
450
+ )
451
+ self._record_tool_call("agent_message_complete")
452
+ return self._response_object(result, "process")
453
+
454
+ def fail_message(self, agent_id: str, message_id: int, error: str) -> dict[str, Any]:
455
+ result = self._request(
456
+ "POST",
457
+ "/agents/messages/fail",
458
+ self._merchant_payload(
459
+ {"agent_id": agent_id, "message_id": _positive_whole_int(message_id, "message_id"), "error": error}
460
+ ),
461
+ )
462
+ self._record_tool_call("agent_message_fail", status="failed", error=error)
463
+ return self._response_object(result, "process")
464
+
465
+ def abandon_message(self, agent_id: str, message_id: int, error: str) -> dict[str, Any]:
466
+ result = self._request(
467
+ "POST",
468
+ "/agents/messages/abandon",
469
+ self._merchant_payload(
470
+ {"agent_id": agent_id, "message_id": _positive_whole_int(message_id, "message_id"), "error": error}
471
+ ),
472
+ )
473
+ self._record_tool_call("agent_message_abandon", error=error)
474
+ return self._response_object(result, "process")
475
+
476
+ def abandon_stale_messages(self, agent_id: str, stale_after_seconds: int = 300) -> list[dict[str, Any]]:
477
+ result = self._request(
478
+ "POST",
479
+ "/agents/messages/abandon-stale",
480
+ self._merchant_payload(
481
+ {
482
+ "agent_id": agent_id,
483
+ "stale_after_seconds": _positive_whole_int(stale_after_seconds, "stale_after_seconds"),
484
+ }
485
+ ),
486
+ )
487
+ self._record_tool_call("agent_message_abandon_stale")
488
+ return self._response_list(result, "abandoned")
@@ -0,0 +1 @@
1
+ """Marketplace API package."""