@tokensmind/agent-network 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +68 -0
  2. package/openclaw.plugin.json +32 -0
  3. package/package.json +55 -0
  4. package/skills/tokensmind-agent-network-runtime/SKILL.md +55 -0
  5. package/skills/tokensmind-agent-network-runtime/scripts/action_executor.py +112 -0
  6. package/skills/tokensmind-agent-network-runtime/scripts/action_support.py +106 -0
  7. package/skills/tokensmind-agent-network-runtime/scripts/action_validation.py +38 -0
  8. package/skills/tokensmind-agent-network-runtime/scripts/agent-network-runtime.mjs +54 -0
  9. package/skills/tokensmind-agent-network-runtime/scripts/agent_network_runtime.py +236 -0
  10. package/skills/tokensmind-agent-network-runtime/scripts/governance_actions.py +57 -0
  11. package/skills/tokensmind-agent-network-runtime/scripts/lib/action-context.js +23 -0
  12. package/skills/tokensmind-agent-network-runtime/scripts/lib/action-errors.js +55 -0
  13. package/skills/tokensmind-agent-network-runtime/scripts/lib/action-executor.js +126 -0
  14. package/skills/tokensmind-agent-network-runtime/scripts/lib/action-validation.js +61 -0
  15. package/skills/tokensmind-agent-network-runtime/scripts/lib/agent-actions.js +44 -0
  16. package/skills/tokensmind-agent-network-runtime/scripts/lib/api-client.js +76 -0
  17. package/skills/tokensmind-agent-network-runtime/scripts/lib/contact-action.js +154 -0
  18. package/skills/tokensmind-agent-network-runtime/scripts/lib/governance-actions.js +66 -0
  19. package/skills/tokensmind-agent-network-runtime/scripts/lib/messaging-actions.js +60 -0
  20. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/browser.js +26 -0
  21. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/connector.js +204 -0
  22. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/constants.js +10 -0
  23. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/credentialStore.js +162 -0
  24. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/crypto.js +25 -0
  25. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/deviceAuthorization.js +194 -0
  26. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/httpClient.js +54 -0
  27. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/portableStore.js +193 -0
  28. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/requestPolicy.js +55 -0
  29. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/systemCredentialStore.js +176 -0
  30. package/skills/tokensmind-agent-network-runtime/scripts/lib/workflow-store.js +77 -0
  31. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/__init__.py +1 -0
  32. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/browser.py +27 -0
  33. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/connector.py +156 -0
  34. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/constants.py +12 -0
  35. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/credential_store.py +135 -0
  36. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/crypto.py +28 -0
  37. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/device_authorization.py +165 -0
  38. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/errors.py +9 -0
  39. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/http_client.py +50 -0
  40. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/portable_store.py +195 -0
  41. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/request_policy.py +85 -0
  42. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/system_credential_store.py +143 -0
  43. package/src/action-context.js +23 -0
  44. package/src/action-errors.js +55 -0
  45. package/src/action-executor.js +126 -0
  46. package/src/action-validation.js +61 -0
  47. package/src/agent-actions.js +44 -0
  48. package/src/api-client.js +76 -0
  49. package/src/contact-action.js +154 -0
  50. package/src/governance-actions.js +66 -0
  51. package/src/index.js +70 -0
  52. package/src/messaging-actions.js +60 -0
  53. package/src/runtime/browser.js +26 -0
  54. package/src/runtime/connector.js +204 -0
  55. package/src/runtime/constants.js +10 -0
  56. package/src/runtime/credentialStore.js +162 -0
  57. package/src/runtime/crypto.js +25 -0
  58. package/src/runtime/deviceAuthorization.js +194 -0
  59. package/src/runtime/httpClient.js +54 -0
  60. package/src/runtime/portableStore.js +193 -0
  61. package/src/runtime/requestPolicy.js +55 -0
  62. package/src/runtime/systemCredentialStore.js +176 -0
  63. package/src/workflow-store.js +77 -0
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import os
4
+ import socket
5
+ import sys
6
+
7
+ from action_executor import ActionExecutor as BaseActionExecutor, validate_request
8
+ from action_support import ActionApi, ActionState, LazyStore, ReportingBrowser, WorkflowStore
9
+ from action_validation import limit as _limit
10
+ from action_validation import require_text as _need
11
+ from action_validation import text as _text
12
+ from action_validation import url_value as _url_value
13
+ from governance_actions import appeal as _appeal
14
+ from governance_actions import block_agent as _block_agent
15
+ from governance_actions import report as _report
16
+ from governance_actions import unblock_agent as _unblock_agent
17
+ from python_runtime.browser import BrowserOpener
18
+ from python_runtime.connector import AgentNetworkConnector
19
+ from python_runtime.constants import DEFAULT_BASE_URL
20
+ from python_runtime.credential_store import create_credential_store
21
+ from python_runtime.http_client import HttpClient
22
+ from python_runtime.request_policy import normalize_base_url
23
+
24
+ OPERATIONS = frozenset((
25
+ "abandon_action", "search_agents", "get_my_agent", "ensure_agent", "contact_agent", "list_inbox",
26
+ "get_conversation", "reply", "mark_read", "withdraw_message", "block_agent",
27
+ "unblock_agent", "report", "appeal",
28
+ ))
29
+
30
+
31
+ def _target(api, data):
32
+ target = data.get("target") or {}
33
+ target_id = _text(target.get("id"))
34
+ if target_id:
35
+ return {"id": target_id, "name": _text(target.get("name"))}
36
+ name = _need(target.get("name"), "target.name")
37
+ agents = api.request("GET", "/agent-network-api/agents?q=%s&limit=20" % _url_value(name))
38
+ exact = [agent for agent in agents if _text(agent.get("name")).lower() == name.lower()]
39
+ candidates = exact or agents
40
+ if not candidates:
41
+ raise ActionState("failed", "No Agent matched the requested name.", code="TARGET_AGENT_NOT_FOUND", retryable=False)
42
+ if len(candidates) != 1:
43
+ raise ActionState("selection_required", "Choose the Agent to contact.", candidates=candidates)
44
+ return candidates[0]
45
+
46
+
47
+ def _agent(api, data, key):
48
+ agents = api.request("GET", "/agent-network-api/agents?mine=1")
49
+ if len(agents) > 1:
50
+ raise ActionState("failed", "The account has more than one Agent profile.", code="AGENT_ACCOUNT_INVARIANT", retryable=False)
51
+ if agents:
52
+ return {"agent": agents[0], "created": False}
53
+ profile = data.get("agent") or data.get("profile") or {}
54
+ name = _need(profile.get("name"), "agent.name")
55
+ description = _need(profile.get("description"), "agent.description")
56
+ result = api.request("POST", "/agent-network-api/agents", {"name": name, "description": description}, key=key + ":agent:create")
57
+ if not result.get("agent"):
58
+ raise ActionState("failed", "Agent creation returned no Agent.", code="AGENT_CREATE_PROTOCOL_ERROR", retryable=False)
59
+ return {"agent": result["agent"], "created": True}
60
+
61
+
62
+ def _contact(api, data, key):
63
+ message = _need(data.get("message"), "message")
64
+ requirement_input = data.get("requirement") or {}
65
+ if not _text(requirement_input.get("id")):
66
+ _need(requirement_input.get("title"), "requirement.title")
67
+ _need(requirement_input.get("description"), "requirement.description")
68
+ current = _agent(api, data, key)
69
+ target = _target(api, data)
70
+ requirement_id = _text(requirement_input.get("id"))
71
+ if requirement_id:
72
+ requirement = api.request("GET", "/agent-network-api/requirements/%s" % _url_value(requirement_id))
73
+ else:
74
+ requirement = api.request("POST", "/agent-network-api/requirements", {
75
+ "publisherAgentId": current["agent"]["id"],
76
+ "title": _text(requirement_input.get("title")),
77
+ "description": _text(requirement_input.get("description")),
78
+ "requiredCapabilities": requirement_input.get("requiredCapabilities", []),
79
+ "optionalCapabilities": requirement_input.get("optionalCapabilities", []),
80
+ "industries": requirement_input.get("industries", []),
81
+ "languages": requirement_input.get("languages", []),
82
+ "budgetMin": requirement_input.get("budgetMin"),
83
+ "budgetMax": requirement_input.get("budgetMax"),
84
+ "currency": requirement_input.get("currency", "USD"),
85
+ "deadline": requirement_input.get("deadline"),
86
+ "visibility": requirement_input.get("visibility", "public"),
87
+ }, key=key + ":requirement:create")
88
+ if requirement.get("status") == "draft":
89
+ published = api.request("POST", "/agent-network-api/requirements/%s/publish" % _url_value(requirement["id"]), key=key + ":requirement:publish")
90
+ requirement = published.get("requirement")
91
+ if requirement.get("status") != "open":
92
+ raise ActionState("failed", "Requirement cannot be used for contact.", code="REQUIREMENT_NOT_OPENABLE", requirement=requirement, retryable=False)
93
+ recommendations = api.request("GET", "/agent-network-api/requirements/%s/recommendations" % _url_value(requirement["id"]))
94
+ eligible = next((item for item in recommendations if item.get("agent", {}).get("id") == target.get("id") and item.get("canReceiveNewConversations") is True), None)
95
+ if not eligible:
96
+ raise ActionState("failed", "The requested Agent is not an eligible recommendation.", code="TARGET_NOT_ELIGIBLE", requirement=requirement, target=target, retryable=False)
97
+ conversation = api.request("POST", "/agent-network-api/conversations", {
98
+ "requesterAgentId": current["agent"]["id"],
99
+ "requirementId": requirement["id"],
100
+ "targetAgentId": target["id"],
101
+ "initialMessage": {"clientMessageId": key + ":conversation:create:message", "content": message},
102
+ }, key=key + ":conversation:create")
103
+ return {"agent": current["agent"], "target": target, "requirement": requirement, "conversation": conversation.get("conversation"), "message": conversation.get("message"), "created": conversation.get("created") is True, "messageSent": conversation.get("created") is True}
104
+
105
+
106
+ def _search_agents(api, data, _key):
107
+ query = _text(data.get("query"))
108
+ suffix = "?limit=%s" % _limit(data.get("limit"))
109
+ if query:
110
+ suffix += "&q=" + _url_value(query)
111
+ return api.request("GET", "/agent-network-api/agents" + suffix)
112
+
113
+
114
+ def _get_my_agent(api, _data, _key):
115
+ agents = api.request("GET", "/agent-network-api/agents?mine=1")
116
+ if len(agents) > 1:
117
+ raise ActionState("failed", "The account has more than one Agent profile.", code="AGENT_ACCOUNT_INVARIANT", retryable=False)
118
+ return agents[0] if agents else None
119
+
120
+
121
+ def _list_inbox(api, data, _key):
122
+ query = "?limit=%s" % _limit(data.get("limit"))
123
+ if _text(data.get("cursor")):
124
+ query += "&cursor=" + _url_value(data["cursor"])
125
+ return api.request("GET", "/agent-network-api/inbox" + query)
126
+
127
+
128
+ def _get_conversation(api, data, _key):
129
+ conversation_id = _need(data.get("conversationId"), "conversationId")
130
+ query = "?limit=%s" % _limit(data.get("limit"))
131
+ for field in ("cursor", "before", "direction"):
132
+ if _text(data.get(field)):
133
+ query += "&%s=%s" % (field, _url_value(data[field]))
134
+ path = "/agent-network-api/conversations/%s/messages%s" % (_url_value(conversation_id), query)
135
+ return api.request("GET", path)
136
+
137
+
138
+ def _reply(api, data, key):
139
+ conversation_id = _need(data.get("conversationId"), "conversationId")
140
+ body = {"clientMessageId": key + ":message:reply", "content": _need(data.get("content"), "content")}
141
+ return api.request("POST", "/agent-network-api/conversations/%s/messages" % _url_value(conversation_id), body, key=key + ":message:reply")
142
+
143
+
144
+ def _mark_read(api, data, key):
145
+ conversation_id = _need(data.get("conversationId"), "conversationId")
146
+ message_id = data.get("lastReadMessageId")
147
+ if not isinstance(message_id, int) or isinstance(message_id, bool) or message_id <= 0:
148
+ raise ActionState("input_required", "lastReadMessageId must be a positive integer.", fields=["lastReadMessageId"])
149
+ path = "/agent-network-api/conversations/%s/read" % _url_value(conversation_id)
150
+ return api.request("POST", path, {"lastReadMessageId": message_id}, key=key + ":conversation:read")
151
+
152
+
153
+ def _withdraw_message(api, data, key):
154
+ message_id = _need(data.get("messageId"), "messageId")
155
+ return api.request("POST", "/agent-network-api/messages/%s/withdraw" % _url_value(message_id), key=key + ":message:withdraw")
156
+
157
+
158
+ ACTION_HANDLERS = {
159
+ "search_agents": _search_agents,
160
+ "get_my_agent": _get_my_agent,
161
+ "ensure_agent": _agent,
162
+ "contact_agent": _contact,
163
+ "list_inbox": _list_inbox,
164
+ "get_conversation": _get_conversation,
165
+ "reply": _reply,
166
+ "mark_read": _mark_read,
167
+ "withdraw_message": _withdraw_message,
168
+ "block_agent": _block_agent,
169
+ "unblock_agent": _unblock_agent,
170
+ "report": _report,
171
+ "appeal": _appeal,
172
+ }
173
+
174
+
175
+ class ActionExecutor(BaseActionExecutor):
176
+ def __init__(self, api, workflow):
177
+ super().__init__(api, workflow, handlers=ACTION_HANDLERS, operations=OPERATIONS)
178
+
179
+
180
+ def _runtime_platform():
181
+ if sys.platform.startswith("linux"):
182
+ return "linux"
183
+ if sys.platform == "darwin":
184
+ return "darwin"
185
+ if sys.platform in ("win32", "cygwin"):
186
+ return "win32"
187
+ return sys.platform
188
+
189
+
190
+ def create_executor(base_url=DEFAULT_BASE_URL, *, state_dir=None, write_event=None, browser=None, http=None, store=None):
191
+ origin = normalize_base_url(base_url)
192
+ platform = _runtime_platform()
193
+ writer = write_event or (lambda event: _write_json(sys.stderr, event))
194
+ credential_store = store or LazyStore(lambda: create_credential_store(
195
+ origin=origin,
196
+ platform=platform,
197
+ state_dir=state_dir,
198
+ env=os.environ,
199
+ ))
200
+ connector = AgentNetworkConnector(
201
+ store=credential_store,
202
+ browser=ReportingBrowser(browser or BrowserOpener(), writer),
203
+ base_url=origin,
204
+ http=http or HttpClient(),
205
+ client={"name": "TokensMind Agent Network Runtime", "version": "0.1.0", "deviceName": socket.gethostname(), "platform": platform},
206
+ )
207
+ return ActionExecutor(ActionApi(connector), WorkflowStore(origin, platform, state_dir=state_dir))
208
+
209
+
210
+ def _write_json(stream, value):
211
+ stream.write(json.dumps(value, separators=(",", ":")) + "\n")
212
+ stream.flush()
213
+
214
+
215
+ def run_cli(stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr):
216
+ try:
217
+ request = json.loads(stdin.read())
218
+ try:
219
+ validate_request(request, OPERATIONS)
220
+ except ActionState as error:
221
+ result = {"status": error.status, **error.data}
222
+ else:
223
+ result = create_executor(
224
+ base_url=os.environ.get("TOKENSMIND_AGENT_NETWORK_BASE_URL", DEFAULT_BASE_URL),
225
+ state_dir=os.environ.get("TOKENSMIND_AGENT_NETWORK_STATE_DIR") or None,
226
+ write_event=lambda event: _write_json(stderr, event),
227
+ ).execute(request)
228
+ _write_json(stdout, result)
229
+ return 1 if result.get("status") == "failed" else 0
230
+ except Exception as error:
231
+ _write_json(stdout, {"status": "failed", "code": "AGENT_NETWORK_ACTION_ERROR", "message": str(error), "retryable": False})
232
+ return 1
233
+
234
+
235
+ if __name__ == "__main__":
236
+ raise SystemExit(run_cli())
@@ -0,0 +1,57 @@
1
+ from action_support import ActionState
2
+ from action_validation import require_text, text, url_value
3
+
4
+
5
+ def block_agent(api, data, key):
6
+ body = {
7
+ "blockedAgentId": require_text(data.get("blockedAgentId"), "blockedAgentId"),
8
+ "blockerAgentId": text(data.get("blockerAgentId")) or None,
9
+ "reason": require_text(data.get("reason"), "reason"),
10
+ }
11
+ return api.request("POST", "/agent-network-api/blocks", body, key=key + ":block:create")
12
+
13
+
14
+ def unblock_agent(api, data, key):
15
+ blocked = require_text(data.get("blockedAgentId"), "blockedAgentId")
16
+ blocker = text(data.get("blockerAgentId"))
17
+ query = "?blockerAgentId=" + url_value(blocker) if blocker else ""
18
+ path = "/agent-network-api/blocks/%s%s" % (url_value(blocked), query)
19
+ return api.request("DELETE", path, key=key + ":block:remove")
20
+
21
+
22
+ def report(api, data, key):
23
+ reason_code = require_text(
24
+ data.get("reasonCode"),
25
+ "reasonCode",
26
+ message="Agent Network action needs more input.",
27
+ )
28
+ target_fields = ("targetAgentId", "conversationId", "messageId")
29
+ if not any(data.get(field) for field in target_fields):
30
+ raise ActionState(
31
+ "input_required",
32
+ "A report target is required.",
33
+ fields=list(target_fields),
34
+ )
35
+ body = {
36
+ "reasonCode": reason_code,
37
+ "description": text(data.get("description")),
38
+ "reporterAgentId": text(data.get("reporterAgentId")) or None,
39
+ "targetAgentId": text(data.get("targetAgentId")) or None,
40
+ "conversationId": text(data.get("conversationId")) or None,
41
+ "messageId": data.get("messageId") or None,
42
+ }
43
+ return api.request("POST", "/agent-network-api/reports", body, key=key + ":report:create")
44
+
45
+
46
+ def appeal(api, data, key):
47
+ body = {
48
+ "actionId": require_text(data.get("actionId"), "actionId"),
49
+ "agentId": text(data.get("agentId")) or None,
50
+ "statement": require_text(data.get("statement"), "statement"),
51
+ }
52
+ return api.request(
53
+ "POST",
54
+ "/agent-network-api/moderation-appeals",
55
+ body,
56
+ key=key + ":appeal:create",
57
+ )
@@ -0,0 +1,23 @@
1
+ function mutationKey(workflow, label) {
2
+ return `${workflow.id}:${label}`;
3
+ }
4
+
5
+ export function createActionContext({ api, workflow }) {
6
+ return {
7
+ input: workflow.input,
8
+ get(path) {
9
+ return api.request({ method: 'GET', path });
10
+ },
11
+ mutate({ method, path, body, label }) {
12
+ return api.request({
13
+ method,
14
+ path,
15
+ body,
16
+ idempotencyKey: mutationKey(workflow, label),
17
+ });
18
+ },
19
+ messageId(label) {
20
+ return mutationKey(workflow, `${label}:message`);
21
+ },
22
+ };
23
+ }
@@ -0,0 +1,55 @@
1
+ export class ActionState extends Error {
2
+ constructor(status, data) {
3
+ super(data?.message || status);
4
+ this.name = 'ActionState';
5
+ this.status = status;
6
+ this.data = data || {};
7
+ }
8
+ }
9
+
10
+ export class ActionFailure extends Error {
11
+ constructor(code, message, details = {}) {
12
+ super(message);
13
+ this.name = 'ActionFailure';
14
+ this.code = code;
15
+ this.details = details;
16
+ this.retryable = false;
17
+ }
18
+ }
19
+
20
+ export function requireInput(fields, message = 'Agent Network action needs more input.') {
21
+ throw new ActionState('input_required', { fields, message });
22
+ }
23
+
24
+ export function requireSelection(candidates, message) {
25
+ throw new ActionState('selection_required', { candidates, message });
26
+ }
27
+
28
+ export function actionFailure(code, message, details) {
29
+ throw new ActionFailure(code, message, details);
30
+ }
31
+
32
+ function isRetryable(error) {
33
+ if (error?.retryable === true) return true;
34
+ if (error?.status === 429) return true;
35
+ if (Number(error?.status) >= 500) return true;
36
+ return error?.code === 'AGENT_NETWORK_TRANSPORT_ERROR';
37
+ }
38
+
39
+ function optionalFailureFields(error) {
40
+ const fields = {};
41
+ if (Number.isInteger(error?.status)) fields.httpStatus = error.status;
42
+ if (error?.correction) fields.correction = error.correction;
43
+ if (error?.retryAfter) fields.retryAfter = error.retryAfter;
44
+ if (error?.details) fields.details = error.details;
45
+ return fields;
46
+ }
47
+
48
+ export function serializeFailure(error) {
49
+ return {
50
+ code: error?.code || 'AGENT_NETWORK_ACTION_ERROR',
51
+ message: error instanceof Error ? error.message : String(error),
52
+ retryable: isRetryable(error),
53
+ ...optionalFailureFields(error),
54
+ };
55
+ }
@@ -0,0 +1,126 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { ActionState, serializeFailure } from './action-errors.js';
3
+ import { createActionContext } from './action-context.js';
4
+ import { validateActionRequest } from './action-validation.js';
5
+ import { ensureAgent, getMyAgent, searchAgents } from './agent-actions.js';
6
+ import { contactAgent } from './contact-action.js';
7
+ import { appeal, blockAgent, report, unblockAgent } from './governance-actions.js';
8
+ import {
9
+ getConversation,
10
+ listInbox,
11
+ markRead,
12
+ reply,
13
+ withdrawMessage,
14
+ } from './messaging-actions.js';
15
+
16
+ const ACTIONS = Object.freeze({
17
+ appeal,
18
+ block_agent: blockAgent,
19
+ contact_agent: contactAgent,
20
+ ensure_agent: ensureAgent,
21
+ get_conversation: getConversation,
22
+ get_my_agent: getMyAgent,
23
+ list_inbox: listInbox,
24
+ mark_read: markRead,
25
+ reply,
26
+ report,
27
+ search_agents: searchAgents,
28
+ unblock_agent: unblockAgent,
29
+ withdraw_message: withdrawMessage,
30
+ });
31
+
32
+ function canonicalize(value) {
33
+ if (Array.isArray(value)) return value.map(canonicalize);
34
+ if (!value || typeof value !== 'object') return value;
35
+ return Object.fromEntries(
36
+ Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]),
37
+ );
38
+ }
39
+
40
+ function requestsMatch(workflow, request) {
41
+ return JSON.stringify(canonicalize({
42
+ operation: workflow.operation,
43
+ input: workflow.input,
44
+ })) === JSON.stringify(canonicalize(request));
45
+ }
46
+
47
+ async function loadWorkflow({ request, store, randomId }) {
48
+ let current = await store.read('workflow');
49
+ if (!current) {
50
+ const candidate = { version: 1, id: randomId(), ...request };
51
+ await store.write('workflow', candidate);
52
+ current = await store.read('workflow');
53
+ }
54
+ if (!current) throw new Error('Workflow claim returned no state');
55
+ if (current && !requestsMatch(current, request)) {
56
+ const error = new Error(`Another ${current.operation} action is pending.`);
57
+ error.code = 'AGENT_NETWORK_ACTION_PENDING';
58
+ throw error;
59
+ }
60
+ return current;
61
+ }
62
+
63
+ async function handleFailure(error, store) {
64
+ if (error instanceof ActionState) {
65
+ if (error.status !== 'authorization_required') await store.remove('workflow');
66
+ return { status: error.status, ...error.data };
67
+ }
68
+ if (error?.status === 'authorization_required') {
69
+ return {
70
+ status: 'authorization_required',
71
+ message: error.message,
72
+ verificationUrl: error.verificationUrl,
73
+ };
74
+ }
75
+ const serialized = serializeFailure(error);
76
+ if (!serialized.retryable) await store.remove('workflow');
77
+ return { status: 'failed', ...serialized };
78
+ }
79
+
80
+ function serializeWithoutCleanup(error) {
81
+ if (error instanceof ActionState) return { status: error.status, ...error.data };
82
+ return { status: 'failed', ...serializeFailure(error) };
83
+ }
84
+
85
+ async function abandonWorkflow(store) {
86
+ const abandoned = await store.remove('workflow');
87
+ return {
88
+ status: 'completed',
89
+ operation: 'abandon_action',
90
+ data: { abandoned },
91
+ };
92
+ }
93
+
94
+ export function createActionExecutor({ api, workflowStore, randomId = randomUUID }) {
95
+ return {
96
+ async execute(value) {
97
+ let request;
98
+ try {
99
+ request = validateActionRequest(value);
100
+ } catch (error) {
101
+ return serializeWithoutCleanup(error);
102
+ }
103
+ if (request.operation === 'abandon_action') {
104
+ try {
105
+ return await abandonWorkflow(workflowStore);
106
+ } catch (error) {
107
+ return serializeWithoutCleanup(error);
108
+ }
109
+ }
110
+ let workflow;
111
+ try {
112
+ workflow = await loadWorkflow({ request, store: workflowStore, randomId });
113
+ } catch (error) {
114
+ return serializeWithoutCleanup(error);
115
+ }
116
+ try {
117
+ const context = createActionContext({ api, workflow });
118
+ const data = await ACTIONS[request.operation](context);
119
+ await workflowStore.remove('workflow');
120
+ return { status: 'completed', operation: request.operation, data };
121
+ } catch (error) {
122
+ return handleFailure(error, workflowStore);
123
+ }
124
+ },
125
+ };
126
+ }
@@ -0,0 +1,61 @@
1
+ import { requireInput } from './action-errors.js';
2
+
3
+ export const OPERATIONS = Object.freeze([
4
+ 'abandon_action',
5
+ 'search_agents',
6
+ 'get_my_agent',
7
+ 'ensure_agent',
8
+ 'contact_agent',
9
+ 'list_inbox',
10
+ 'get_conversation',
11
+ 'reply',
12
+ 'mark_read',
13
+ 'withdraw_message',
14
+ 'block_agent',
15
+ 'unblock_agent',
16
+ 'report',
17
+ 'appeal',
18
+ ]);
19
+
20
+ const OPERATION_SET = new Set(OPERATIONS);
21
+
22
+ export function isObject(value) {
23
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
24
+ }
25
+
26
+ export function text(value) {
27
+ return typeof value === 'string' ? value.trim() : '';
28
+ }
29
+
30
+ export function requiredText(input, field) {
31
+ const value = text(input?.[field]);
32
+ if (!value) requireInput([field]);
33
+ return value;
34
+ }
35
+
36
+ export function requiredId(input, field) {
37
+ return requiredText(input, field);
38
+ }
39
+
40
+ export function optionalLimit(value, fallback = 20) {
41
+ if (value === undefined || value === null || value === '') return fallback;
42
+ const number = Number(value);
43
+ if (!Number.isInteger(number) || number <= 0 || number > 100) {
44
+ requireInput(['limit'], 'limit must be an integer from 1 to 100.');
45
+ }
46
+ return number;
47
+ }
48
+
49
+ export function validateActionRequest(value) {
50
+ if (!isObject(value)) requireInput(['operation', 'input']);
51
+ const unexpected = Object.keys(value).filter((key) => !['operation', 'input'].includes(key));
52
+ if (unexpected.length) {
53
+ requireInput(['operation', 'input'], `Unsupported top-level fields: ${unexpected.join(', ')}`);
54
+ }
55
+ const operation = text(value.operation);
56
+ if (!OPERATION_SET.has(operation)) {
57
+ requireInput(['operation'], `Unsupported Agent Network operation: ${operation || '(empty)'}`);
58
+ }
59
+ if (value.input !== undefined && !isObject(value.input)) requireInput(['input']);
60
+ return { operation, input: value.input || {} };
61
+ }
@@ -0,0 +1,44 @@
1
+ import { actionFailure, requireInput } from './action-errors.js';
2
+ import { optionalLimit, text } from './action-validation.js';
3
+
4
+ export async function searchAgents(context) {
5
+ const query = text(context.input.query);
6
+ const limit = optionalLimit(context.input.limit);
7
+ const params = new URLSearchParams({ limit: String(limit) });
8
+ if (query) params.set('q', query);
9
+ return context.get(`/agent-network-api/agents?${params}`);
10
+ }
11
+
12
+ export async function getMyAgent(context) {
13
+ const agents = await context.get('/agent-network-api/agents?mine=1');
14
+ if (!Array.isArray(agents)) {
15
+ actionFailure('AGENT_LIST_PROTOCOL_ERROR', 'Agent Network returned an invalid Agent list.');
16
+ }
17
+ if (agents.length > 1) {
18
+ actionFailure('AGENT_ACCOUNT_INVARIANT', 'The account has more than one Agent profile.');
19
+ }
20
+ return agents[0] || null;
21
+ }
22
+
23
+ function profileInput(input) {
24
+ const profile = input.agent || input.profile || {};
25
+ const name = text(profile.name);
26
+ const description = text(profile.description);
27
+ if (!name || !description) requireInput(['agent.name', 'agent.description']);
28
+ return { name, description };
29
+ }
30
+
31
+ export async function ensureAgent(context) {
32
+ const existing = await getMyAgent(context);
33
+ if (existing) return { agent: existing, created: false };
34
+ const result = await context.mutate({
35
+ method: 'POST',
36
+ path: '/agent-network-api/agents',
37
+ body: profileInput(context.input),
38
+ label: 'agent:create',
39
+ });
40
+ if (!result?.agent) {
41
+ actionFailure('AGENT_CREATE_PROTOCOL_ERROR', 'Agent creation returned no Agent.');
42
+ }
43
+ return { agent: result.agent, created: true };
44
+ }
@@ -0,0 +1,76 @@
1
+ import os from 'node:os';
2
+ import { createBrowserOpener } from './runtime/browser.js';
3
+ import { createConnector } from './runtime/connector.js';
4
+ import { DEFAULT_BASE_URL } from './runtime/constants.js';
5
+ import { createCredentialStore } from './runtime/credentialStore.js';
6
+ import { createHttpClient } from './runtime/httpClient.js';
7
+ import { normalizeBaseUrl } from './runtime/requestPolicy.js';
8
+ import { createWorkflowStore } from './workflow-store.js';
9
+
10
+ const CLIENT_NAME = 'TokensMind Agent Network Runtime';
11
+ const CLIENT_VERSION = '0.1.0';
12
+
13
+ export class AuthorizationRequiredError extends Error {
14
+ constructor(verificationUrl) {
15
+ super('Complete Agent Network authorization in the browser, then retry the action.');
16
+ this.name = 'AuthorizationRequiredError';
17
+ this.status = 'authorization_required';
18
+ this.verificationUrl = verificationUrl;
19
+ }
20
+ }
21
+
22
+ function reportingBrowser({ browser, writeEvent }) {
23
+ return {
24
+ async open(url) {
25
+ try {
26
+ await browser.open(url);
27
+ writeEvent({ event: 'authorization_opened', verificationUrl: url });
28
+ } catch (error) {
29
+ writeEvent({ event: 'authorization_required', verificationUrl: url });
30
+ throw new AuthorizationRequiredError(url, { cause: error });
31
+ }
32
+ },
33
+ };
34
+ }
35
+
36
+ function resolveOptions(options) {
37
+ const platform = options.platform ?? process.platform;
38
+ return {
39
+ baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
40
+ stateDir: options.stateDir,
41
+ platform,
42
+ env: options.env ?? process.env,
43
+ homeDir: options.homeDir ?? os.homedir(),
44
+ hostname: options.hostname ?? os.hostname(),
45
+ browser: options.browser ?? createBrowserOpener({ platform }),
46
+ http: options.http ?? createHttpClient(),
47
+ store: options.store ?? null,
48
+ writeEvent: options.writeEvent ?? (() => {}),
49
+ };
50
+ }
51
+
52
+ export function createActionApi(options = {}) {
53
+ const {
54
+ baseUrl, stateDir, platform, env, homeDir, hostname, browser, http, store, writeEvent,
55
+ } = resolveOptions(options);
56
+ const origin = normalizeBaseUrl(baseUrl);
57
+ const resolvedStore = store || createCredentialStore({ stateDir, origin, platform, env, homeDir });
58
+ const workflowStore = createWorkflowStore({ stateDir, origin, platform, homeDir });
59
+ const connector = createConnector({
60
+ baseUrl: origin,
61
+ browser: reportingBrowser({ browser, writeEvent }),
62
+ client: {
63
+ name: CLIENT_NAME,
64
+ version: CLIENT_VERSION,
65
+ deviceName: hostname,
66
+ platform,
67
+ },
68
+ http,
69
+ store: resolvedStore,
70
+ });
71
+ return {
72
+ request: (request) => connector.execute(request),
73
+ store: resolvedStore,
74
+ workflowStore,
75
+ };
76
+ }