axel-protocol 2.3.0__tar.gz → 2.3.2__tar.gz

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 (38) hide show
  1. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/PKG-INFO +1 -1
  2. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/axel/cli.py +57 -1
  3. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/axel/server.py +57 -0
  4. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/axel/static/monitor.html +191 -1
  5. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/pyproject.toml +1 -1
  6. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/.github/workflows/ci.yml +0 -0
  7. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/.github/workflows/publish.yml +0 -0
  8. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/.gitignore +0 -0
  9. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/CHANGELOG.md +0 -0
  10. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/CONTRIBUTING.md +0 -0
  11. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/Dockerfile +0 -0
  12. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/LICENSE +0 -0
  13. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/README.md +0 -0
  14. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/axel/__init__.py +0 -0
  15. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/axel/adapters/__init__.py +0 -0
  16. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/axel/adapters/bedrock.py +0 -0
  17. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/axel/adapters/cohere.py +0 -0
  18. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/axel/adapters/gemini.py +0 -0
  19. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/axel/adapters/groq.py +0 -0
  20. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/axel/adapters/litellm.py +0 -0
  21. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/axel/adapters/mistral.py +0 -0
  22. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/axel/adapters/together.py +0 -0
  23. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/axel/client.py +0 -0
  24. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/axel/core.py +0 -0
  25. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/axel/learning.py +0 -0
  26. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/axel/persistence.py +0 -0
  27. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/axel/testing.py +0 -0
  28. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/axel/universal.py +0 -0
  29. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/docker-compose.yml +0 -0
  30. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/docs/message-schemas.md +0 -0
  31. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/examples/demo_live.py +0 -0
  32. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/install.sh +0 -0
  33. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/tests/__init__.py +0 -0
  34. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/tests/test_adapters.py +0 -0
  35. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/tests/test_client.py +0 -0
  36. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/tests/test_core.py +0 -0
  37. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/tests/test_new_features.py +0 -0
  38. {axel_protocol-2.3.0 → axel_protocol-2.3.2}/tests/test_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: axel-protocol
3
- Version: 2.3.0
3
+ Version: 2.3.2
4
4
  Summary: AXEL — Agent eXchange Language: a universal protocol for multi-LLM networks
5
5
  Project-URL: Homepage, https://github.com/sectorx/axel-protocol
6
6
  Project-URL: Repository, https://github.com/sectorx/axel-protocol
@@ -427,6 +427,27 @@ def _fire_task(server: str, fr: str, to: str, action: str, body: dict) -> None:
427
427
  pass # demo continues even if event fire fails
428
428
 
429
429
 
430
+ def _fire_progress(server: str, run: int, step: int, total: int, agent: str, action: str) -> None:
431
+ """Broadcast pipeline progress so the dashboard can show a step indicator."""
432
+ import json as _json
433
+ import urllib.request as _ur
434
+ import uuid as _uuid
435
+ msg = {
436
+ "t": "PROG", "v": "2.1",
437
+ "id": str(_uuid.uuid4()),
438
+ "fr": "axel-demo", "to": "dashboard",
439
+ "body": {"run": run, "step": step, "total": total,
440
+ "agent": agent, "action": action},
441
+ }
442
+ data = _json.dumps(msg).encode()
443
+ req = _ur.Request(f"{server}/send", data=data,
444
+ headers={"Content-Type": "application/json"}, method="POST")
445
+ try:
446
+ _ur.urlopen(req, timeout=3)
447
+ except Exception:
448
+ pass
449
+
450
+
430
451
  def _fire_result(server: str, fr: str, to: str, action: str, body: dict) -> None:
431
452
  """POST an AXEL OK result message through the server so SSE/dashboard sees it."""
432
453
  import json as _json
@@ -657,6 +678,24 @@ def cmd_demo(args): # noqa: C901
657
678
  "Building trust and reliability in multi-agent systems",
658
679
  ]
659
680
 
681
+ def _check_user_queue(srv):
682
+ """Check if a user submitted a task via the dashboard. Returns task string or None."""
683
+ try:
684
+ resp = _ur.urlopen(f"{srv}/queue", timeout=3)
685
+ data = json.loads(resp.read())
686
+ return data.get("task") # None if queue was empty
687
+ except Exception:
688
+ return None
689
+
690
+ def _fetch_topic_library(srv):
691
+ """Fetch user-defined learning topics from the server. Returns list or []."""
692
+ try:
693
+ resp = _ur.urlopen(f"{srv}/topics", timeout=3)
694
+ data = json.loads(resp.read())
695
+ return data.get("topics", [])
696
+ except Exception:
697
+ return []
698
+
660
699
  def _fetch_memory_context(srv):
661
700
  """Pull top lessons from shared memory and format as context."""
662
701
  import json
@@ -674,12 +713,16 @@ def cmd_demo(args): # noqa: C901
674
713
  except Exception:
675
714
  return ""
676
715
 
716
+ step_counter = [0] # mutable so inner func can update it
717
+
677
718
  def step(fr, to, action, prompt, learn_key=None, learn_insight=None, mem_ctx=""):
678
719
  """Fire a task, call the LLM (or use mock), fire the result."""
720
+ step_counter[0] += 1
679
721
  full_prompt = (mem_ctx + prompt) if mem_ctx else prompt
680
722
  mdl = _agent_model(to)
681
723
  mdl_short = mdl.split("/")[-1].replace(":free", "") if mdl != "auto/free" else "free"
682
724
  print(f"\n {fr} → {to} [{action}] ({mdl_short})")
725
+ _fire_progress(server, run_count, step_counter[0], 5, to, action)
683
726
  _fire_task(server, fr, to, action, {"prompt": prompt[:200]})
684
727
  if mock:
685
728
  time.sleep(0.8)
@@ -713,7 +756,20 @@ def cmd_demo(args): # noqa: C901
713
756
  try:
714
757
  while True:
715
758
  run_count += 1
716
- topic = _TOPICS[(run_count - 1) % len(_TOPICS)]
759
+ step_counter[0] = 0 # reset step counter for new run
760
+ # Check if user submitted a one-off task from the dashboard
761
+ user_task = _check_user_queue(server)
762
+ if user_task:
763
+ topic = user_task
764
+ print(f"\n ⌨️ User task received: \"{topic}\"")
765
+ else:
766
+ # Pull user-defined topic library — use it if populated
767
+ user_topics = _fetch_topic_library(server)
768
+ if user_topics:
769
+ topic = user_topics[(run_count - 1) % len(user_topics)]
770
+ print(f"\n 📚 Topic Library ({len(user_topics)} topics) — exploring: \"{topic}\"")
771
+ else:
772
+ topic = _TOPICS[(run_count - 1) % len(_TOPICS)]
717
773
 
718
774
  # Fetch lessons learned so far — injected into prompts so agents remember
719
775
  mem_ctx = _fetch_memory_context(server) if run_count > 1 else ""
@@ -375,6 +375,12 @@ class AXELServer:
375
375
  # Liveness: agent_id → last-seen timestamp (unix seconds)
376
376
  self._last_seen: Dict[str, float] = {}
377
377
  self._liveness_lock = threading.Lock()
378
+ # User task queue — dashboard POSTs tasks here; demo loop pops them
379
+ self._task_queue: list = []
380
+ self._task_queue_lock = threading.Lock()
381
+ # User-defined topic library — agents explore these instead of built-in topics
382
+ self._topic_library: list = []
383
+ self._topic_lock = threading.Lock()
378
384
  # Restore persisted agents/memory if db is attached
379
385
  if self.persistence:
380
386
  self._restore_from_db()
@@ -927,6 +933,57 @@ class AXELServer:
927
933
  result["db_stats"] = self.persistence.stats()
928
934
  return result
929
935
 
936
+ # ── User Task Queue ───────────────────────────────────────
937
+ @app.post("/queue")
938
+ async def queue_task(request: Request):
939
+ """Dashboard submits a user task here; the demo loop picks it up."""
940
+ body = await request.json()
941
+ task = (body.get("task") or "").strip()
942
+ if not task:
943
+ return {"ok": False, "error": "task is required"}
944
+ with self._task_queue_lock:
945
+ self._task_queue.append(task)
946
+ # Broadcast so the dashboard knows the task was received
947
+ self.event_bus.publish("user.task", {"task": task, "queued": len(self._task_queue)})
948
+ return {"ok": True, "queued": len(self._task_queue)}
949
+
950
+ @app.get("/queue")
951
+ async def peek_queue():
952
+ """Pop and return the next user task, or null if queue is empty."""
953
+ with self._task_queue_lock:
954
+ task = self._task_queue.pop(0) if self._task_queue else None
955
+ return {"task": task, "remaining": len(self._task_queue)}
956
+
957
+ # ── Topic Library ─────────────────────────────────────────
958
+ @app.post("/topics")
959
+ async def add_topic(request: Request):
960
+ """Add a topic to the learning library."""
961
+ body = await request.json()
962
+ topic = (body.get("topic") or "").strip()
963
+ if not topic:
964
+ return {"ok": False, "error": "topic is required"}
965
+ with self._topic_lock:
966
+ if topic not in self._topic_library:
967
+ self._topic_library.append(topic)
968
+ self.event_bus.publish("topics.updated", {"topics": list(self._topic_library)})
969
+ return {"ok": True, "topics": list(self._topic_library)}
970
+
971
+ @app.get("/topics")
972
+ async def list_topics():
973
+ """Return all user-defined topics."""
974
+ with self._topic_lock:
975
+ return {"topics": list(self._topic_library)}
976
+
977
+ @app.delete("/topics/{idx}")
978
+ async def remove_topic(idx: int):
979
+ """Remove a topic by index."""
980
+ with self._topic_lock:
981
+ if idx < 0 or idx >= len(self._topic_library):
982
+ return {"ok": False, "error": "index out of range"}
983
+ removed = self._topic_library.pop(idx)
984
+ self.event_bus.publish("topics.updated", {"topics": list(self._topic_library)})
985
+ return {"ok": True, "removed": removed, "topics": list(self._topic_library)}
986
+
930
987
  # ── SSE Event Stream ─────────────────────────────────────
931
988
  @app.get("/stream")
932
989
  async def event_stream(request: Request):
@@ -63,6 +63,30 @@ header {
63
63
  .stat-val { font-size: 20px; font-weight: 700; color: var(--accent); line-height: 1; }
64
64
  .stat-lbl { font-size: 9px; text-transform: uppercase; letter-spacing: .8px; color: var(--muted); margin-top: 2px; }
65
65
 
66
+ /* ── Mission input bar ── */
67
+ .mission-bar {
68
+ display: flex; align-items: center; gap: 10px;
69
+ padding: 8px 18px; background: rgba(129,140,248,.04);
70
+ border-bottom: 1px solid var(--border); flex-shrink: 0;
71
+ }
72
+ .mission-input {
73
+ flex: 1; background: var(--bg); border: 1px solid var(--border); color: var(--text);
74
+ padding: 7px 12px; border-radius: 7px; font-size: 12px;
75
+ transition: border-color .2s;
76
+ }
77
+ .mission-input:focus { outline: none; border-color: var(--accent); }
78
+ .mission-input::placeholder { color: var(--muted); }
79
+ .mission-btn {
80
+ background: var(--accent); color: #fff; border: none;
81
+ padding: 7px 16px; border-radius: 7px; font-size: 12px; font-weight: 600; cursor: pointer;
82
+ white-space: nowrap; transition: opacity .2s;
83
+ }
84
+ .mission-btn:hover { opacity: .85; }
85
+ .mission-btn:disabled { opacity: .4; cursor: default; }
86
+ .mission-status { font-size: 11px; color: var(--muted); white-space: nowrap; min-width: 120px; }
87
+ .mission-status.sent { color: var(--green); }
88
+ .mission-status.waiting { color: var(--yellow); }
89
+
66
90
  /* ── Progress strip ── */
67
91
  .progress-strip {
68
92
  display: none; align-items: center; gap: 14px;
@@ -199,6 +223,37 @@ header {
199
223
  .conf-bar { height: 2px; background: var(--border); border-radius: 2px; margin-top: 5px; }
200
224
  .conf-fill { height: 100%; background: var(--green); border-radius: 2px; transition: width .5s; }
201
225
 
226
+ /* ── Topic Library bar ── */
227
+ .topic-bar {
228
+ display: flex; align-items: flex-start; gap: 10px;
229
+ padding: 8px 18px; background: rgba(251,191,36,.03);
230
+ border-bottom: 1px solid var(--border); flex-shrink: 0;
231
+ }
232
+ .topic-bar-label { font-size: 10px; text-transform: uppercase; letter-spacing: .7px; color: var(--yellow); font-weight: 700; }
233
+ .topic-bar-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 5px; }
234
+ .topic-bar-row { display: flex; align-items: center; gap: 8px; }
235
+ .topic-input {
236
+ flex: 1; background: var(--bg); border: 1px solid var(--border); color: var(--text);
237
+ padding: 5px 10px; border-radius: 6px; font-size: 12px; min-width: 0;
238
+ }
239
+ .topic-input:focus { outline: none; border-color: var(--yellow); }
240
+ .topic-add-btn {
241
+ padding: 5px 12px; border-radius: 6px; border: 1px solid rgba(251,191,36,.4);
242
+ background: rgba(251,191,36,.08); color: var(--yellow); font-size: 12px; cursor: pointer;
243
+ white-space: nowrap; transition: background .15s;
244
+ }
245
+ .topic-add-btn:hover { background: rgba(251,191,36,.18); }
246
+ .topic-chips { display: flex; flex-wrap: wrap; gap: 5px; }
247
+ .topic-chip {
248
+ display: inline-flex; align-items: center; gap: 5px;
249
+ background: rgba(251,191,36,.1); border: 1px solid rgba(251,191,36,.25);
250
+ border-radius: 20px; padding: 3px 8px 3px 10px; font-size: 11px; color: var(--text);
251
+ }
252
+ .topic-chip-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 200px; }
253
+ .topic-chip-x { cursor: pointer; color: var(--muted); font-size: 14px; line-height: 1; flex-shrink: 0; transition: color .15s; }
254
+ .topic-chip-x:hover { color: var(--red); }
255
+ .topic-empty { font-size: 11px; color: var(--muted); }
256
+
202
257
  /* ── Settings overlay ── */
203
258
  .settings-overlay {
204
259
  position: fixed; inset: 0; z-index: 200;
@@ -289,6 +344,27 @@ header {
289
344
  <div class="stat"><div class="stat-val" id="sUptime">–</div><div class="stat-lbl">Uptime</div></div>
290
345
  </div>
291
346
 
347
+ <div class="mission-bar">
348
+ <span style="font-size:13px">🎯</span>
349
+ <input class="mission-input" id="missionInput" placeholder="Give the agents a task… e.g. 'Research the impact of AI on creative industries'" />
350
+ <button class="mission-btn" id="missionBtn" onclick="submitMission()">Send to Agents →</button>
351
+ <span class="mission-status" id="missionStatus">agents running autonomously</span>
352
+ </div>
353
+
354
+ <div class="topic-bar">
355
+ <span style="font-size:15px;line-height:1;padding-top:1px">📚</span>
356
+ <div class="topic-bar-body">
357
+ <div class="topic-bar-label">Learning Topics <span style="font-weight:400;text-transform:none;letter-spacing:0;color:var(--muted)">— agents will cycle through these instead of built-in topics</span></div>
358
+ <div class="topic-bar-row">
359
+ <input class="topic-input" id="topicInput" placeholder="Add a topic… e.g. 'The ethics of autonomous AI systems'" />
360
+ <button class="topic-add-btn" onclick="addTopic()">+ Add Topic</button>
361
+ </div>
362
+ <div class="topic-chips" id="topicChips">
363
+ <span class="topic-empty">No topics yet — agents use built-in rotation</span>
364
+ </div>
365
+ </div>
366
+ </div>
367
+
292
368
  <div class="progress-strip" id="progressStrip">
293
369
  <div class="prog-label" id="progLabel">Run #1</div>
294
370
  <div class="prog-dots">
@@ -422,7 +498,7 @@ function reconnect() {
422
498
  function connect() {
423
499
  setConn(false);
424
500
  sse = new EventSource(serverUrl + '/stream');
425
- sse.onopen = () => { setConn(true); poll(); };
501
+ sse.onopen = () => { setConn(true); poll(); loadTopics(); };
426
502
  sse.onerror = () => {
427
503
  setConn(false);
428
504
  if (document.getElementById('toggleAR').classList.contains('on')) {
@@ -533,10 +609,20 @@ function onEvent(d) {
533
609
  pulseAgent(from, 'learning');
534
610
  setTimeout(poll, 600);
535
611
 
612
+ } else if (type === 'user.task') {
613
+ const task = p.task || p.data?.task || '';
614
+ addFeed('announce', '🎯', `<b>User</b> sent a mission: <i>${esc(task.slice(0,80))}${task.length>80?'…':''}</i>`, 'Agents will pick this up on the next run', { rawType:'USER', fullPrompt: task });
615
+ const status = document.getElementById('missionStatus');
616
+ if (status) { status.textContent = '⏳ waiting for agents…'; status.className = 'mission-status waiting'; }
617
+
536
618
  } else if (p.t === 'PROG' || type.includes('progress')) {
537
619
  const b = p.body || p;
538
620
  updateProgress(b.run ?? 1, b.step ?? 1, b.total ?? 5, b.agent || '', b.action || '');
539
621
 
622
+ } else if (type === 'topics.updated') {
623
+ topicLibrary = p.topics || [];
624
+ renderTopics();
625
+
540
626
  } else if (type.includes('error')) {
541
627
  const msg = p.body?.error || p.error || '';
542
628
  addFeed('error', '❌', `Error from <b>${p.fr||'?'}</b>`, msg, { rawType:'ERR', from:p.fr, fullText:msg });
@@ -901,6 +987,110 @@ function toggleMem() {
901
987
  document.getElementById('memHint').textContent = memExpanded ? '▴ collapse' : '▾ expand';
902
988
  }
903
989
 
990
+ // ─── Mission input ─────────────────────────────────────────
991
+ function submitMission() {
992
+ const input = document.getElementById('missionInput');
993
+ const btn = document.getElementById('missionBtn');
994
+ const status = document.getElementById('missionStatus');
995
+ const task = input.value.trim();
996
+ if (!task) { input.focus(); return; }
997
+
998
+ btn.disabled = true;
999
+ status.textContent = 'sending…';
1000
+ status.className = 'mission-status';
1001
+
1002
+ fetch(serverUrl + '/queue', {
1003
+ method: 'POST',
1004
+ headers: { 'Content-Type': 'application/json' },
1005
+ body: JSON.stringify({ task }),
1006
+ })
1007
+ .then(r => r.json())
1008
+ .then(d => {
1009
+ if (d.ok) {
1010
+ status.textContent = '✓ queued — agents will pick it up next run';
1011
+ status.className = 'mission-status sent';
1012
+ input.value = '';
1013
+ setTimeout(() => {
1014
+ status.textContent = 'agents running autonomously';
1015
+ status.className = 'mission-status';
1016
+ btn.disabled = false;
1017
+ }, 4000);
1018
+ } else {
1019
+ status.textContent = 'error: ' + (d.error || 'unknown');
1020
+ btn.disabled = false;
1021
+ }
1022
+ })
1023
+ .catch(() => {
1024
+ status.textContent = 'could not reach server';
1025
+ status.className = 'mission-status';
1026
+ btn.disabled = false;
1027
+ });
1028
+ }
1029
+
1030
+ // Allow Enter key to submit
1031
+ document.addEventListener('keydown', e => {
1032
+ if (e.key === 'Enter' && document.activeElement.id === 'missionInput') submitMission();
1033
+ if (e.key === 'Enter' && document.activeElement.id === 'topicInput') addTopic();
1034
+ });
1035
+
1036
+ // ─── Topic Library ─────────────────────────────────────────────
1037
+ let topicLibrary = [];
1038
+
1039
+ function renderTopics() {
1040
+ const chips = document.getElementById('topicChips');
1041
+ if (!chips) return;
1042
+ if (topicLibrary.length === 0) {
1043
+ chips.innerHTML = '<span class="topic-empty">No topics yet — agents use built-in rotation</span>';
1044
+ return;
1045
+ }
1046
+ chips.innerHTML = topicLibrary.map((t, i) =>
1047
+ `<span class="topic-chip">
1048
+ <span class="topic-chip-text" title="${esc(t)}">${esc(t)}</span>
1049
+ <span class="topic-chip-x" onclick="removeTopic(${i})" title="Remove">×</span>
1050
+ </span>`
1051
+ ).join('');
1052
+ }
1053
+
1054
+ function addTopic() {
1055
+ const input = document.getElementById('topicInput');
1056
+ const topic = (input.value || '').trim();
1057
+ if (!topic) { input.focus(); return; }
1058
+ fetch(serverUrl + '/topics', {
1059
+ method: 'POST',
1060
+ headers: {'Content-Type':'application/json'},
1061
+ body: JSON.stringify({topic})
1062
+ })
1063
+ .then(r => r.json())
1064
+ .then(d => {
1065
+ if (d.ok) {
1066
+ topicLibrary = d.topics;
1067
+ input.value = '';
1068
+ renderTopics();
1069
+ addFeed('announce', '📚', `<b>Topic added:</b> ${esc(topic.slice(0,80))}`, 'Agents will explore this on the next run', {rawType:'TOPIC'});
1070
+ }
1071
+ })
1072
+ .catch(() => {});
1073
+ }
1074
+
1075
+ function removeTopic(idx) {
1076
+ fetch(serverUrl + `/topics/${idx}`, { method: 'DELETE' })
1077
+ .then(r => r.json())
1078
+ .then(d => {
1079
+ if (d.ok !== false) {
1080
+ topicLibrary = d.topics;
1081
+ renderTopics();
1082
+ }
1083
+ })
1084
+ .catch(() => {});
1085
+ }
1086
+
1087
+ function loadTopics() {
1088
+ fetch(serverUrl + '/topics')
1089
+ .then(r => r.json())
1090
+ .then(d => { topicLibrary = d.topics || []; renderTopics(); })
1091
+ .catch(() => {});
1092
+ }
1093
+
904
1094
  // ─── Settings ──────────────────────────────────────────────────
905
1095
  function openSettings() {
906
1096
  document.getElementById('settingsUrl').value = serverUrl;
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "axel-protocol"
7
- version = "2.3.0"
7
+ version = "2.3.2"
8
8
  description = "AXEL — Agent eXchange Language: a universal protocol for multi-LLM networks"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
File without changes
File without changes
File without changes
File without changes
File without changes