diff --git a/.gitignore b/.gitignore index b35ce92..d000df5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,10 @@ **/.venv-e2e/ **/node_modules/ **/__pycache__/ +dist/ +package-lock.json +go.sum +**/target/ +**/bin/Debug/ +**/bin/Release/ +**/obj/ diff --git a/cloud/README.md b/cloud/README.md deleted file mode 100644 index 0f5b3a0..0000000 --- a/cloud/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Cloud Examples Index - -These examples require AXME Cloud runtime and an API key. - -Use this path for product onboarding and lifecycle orchestration scenarios: - -- durable execution -- retries and backoff -- callbacks and multi-service coordination -- workflow controls and runtime lifecycle events - -## Canonical runnable set - -- `../examples/approval-workflow` -- `../examples/external-callback` -- `../examples/retry-workflow` -- `../examples/multi-service-coordination` - -Cloud aliases (stable path layout, no code duplication): - -- `approval-workflow/` -- `external-callback/` -- `retry-workflow/` -- `multi-service-coordination/` - -## Requirements - -- `AXME_API_KEY` (service-account key from `https://cloud.axme.ai/alpha`) -- optional `AXME_BASE_URL` override (default `https://api.cloud.axme.ai`) - -For protocol-only examples that do not require AXME Cloud, see `../protocol/README.md`. diff --git a/cloud/approval-workflow/README.md b/cloud/approval-workflow/README.md deleted file mode 100644 index 362a0a9..0000000 --- a/cloud/approval-workflow/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Cloud Example Alias: approval-workflow - -Canonical runnable source: - -- `../../examples/approval-workflow` - -This alias path exists to keep a clear `cloud/*` layout without duplicating runnable code. diff --git a/cloud/external-callback/README.md b/cloud/external-callback/README.md deleted file mode 100644 index a77a318..0000000 --- a/cloud/external-callback/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Cloud Example Alias: external-callback - -Canonical runnable source: - -- `../../examples/external-callback` - -This alias path exists to keep a clear `cloud/*` layout without duplicating runnable code. diff --git a/cloud/multi-service-coordination/README.md b/cloud/multi-service-coordination/README.md deleted file mode 100644 index 870c595..0000000 --- a/cloud/multi-service-coordination/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Cloud Example Alias: multi-service-coordination - -Canonical runnable source: - -- `../../examples/multi-service-coordination` - -This alias path exists to keep a clear `cloud/*` layout without duplicating runnable code. diff --git a/cloud/retry-workflow/README.md b/cloud/retry-workflow/README.md deleted file mode 100644 index cec4e9e..0000000 --- a/cloud/retry-workflow/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Cloud Example Alias: retry-workflow - -Canonical runnable source: - -- `../../examples/retry-workflow` - -This alias path exists to keep a clear `cloud/*` layout without duplicating runnable code. diff --git a/examples/approval-workflow/.env.example b/examples/approval-workflow/.env.example deleted file mode 100644 index a1c0ff1..0000000 --- a/examples/approval-workflow/.env.example +++ /dev/null @@ -1,9 +0,0 @@ -# Optional override. If omitted, example uses AXME Cloud default endpoint. -AXME_BASE_URL=https://api.cloud.axme.ai -AXME_API_KEY=replace-with-api-key -# Optional for routes requiring actor context. -AXME_ACTOR_TOKEN= - -AXME_FROM_AGENT=agent://examples/requester -AXME_TO_AGENT=agent://examples/approver -AXME_APPROVAL_OWNER_AGENT=agent://examples/requester diff --git a/examples/approval-workflow/README.md b/examples/approval-workflow/README.md deleted file mode 100644 index 7854d0f..0000000 --- a/examples/approval-workflow/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# Approval Workflow - -Problem: your operation requires approval semantics, but the product path must stay fast. -Goal: keep one durable intent lifecycle and finish without losing state. - -This example demonstrates: - -- intent creation (`POST /v1/intents`) -- automatic approval via immediate `resume` -- terminal completion via `resolve` -- lifecycle event observation - -## Requirements - -This example runs against **AXME Cloud**. - -You need: - -- AXME Cloud API key (generated on the landing page) -- `.env` file with `AXME_API_KEY` set (copy from `.env.example`) -- optional `AXME_BASE_URL` override (defaults to AXME Cloud endpoint) - -Get API key at: - -- - -```mermaid -sequenceDiagram - participant App as Client App - participant AXME as AXME Cloud Runtime - - App->>AXME: create intent - App->>AXME: resume intent (auto-approved, no manual wait) - App->>AXME: resolve COMPLETED - AXME-->>App: terminal lifecycle event -``` - -## Run (Python) - -```bash -cd examples/approval-workflow/python -python -m venv .venv -source .venv/bin/activate -pip install -r requirements.txt -cp ../.env.example ../.env -# edit ../.env and set AXME_API_KEY -# optional override: -# export AXME_BASE_URL="https://api.cloud.axme.ai" -python main.py -``` - -## Run (TypeScript) - -```bash -cd examples/approval-workflow/typescript -npm install -cp ../.env.example ../.env -# edit ../.env and set AXME_API_KEY -# optional override: -# export AXME_BASE_URL="https://api.cloud.axme.ai" -npm run start -``` - -## Notes - -- This scenario is configured as an automatic approval path. -- The example does not wait for manual/human approval. - -## Additional SDK snippets - -See [`../../snippets/README.md`](../../snippets/README.md). - -Built using AXME (AXP). diff --git a/examples/approval-workflow/python/main.py b/examples/approval-workflow/python/main.py deleted file mode 100644 index 0ded878..0000000 --- a/examples/approval-workflow/python/main.py +++ /dev/null @@ -1,855 +0,0 @@ -""" -approval-workflow — Axme SDK example (Model B / ScenarioBundle) -=============================================================== -Demonstrates a durable 3-step approval workflow using POST /v1/scenarios/apply. -The server provisions agents, compiles the workflow DAG, and drives all step -transitions autonomously. The client only observes the event stream and handles -the one human-input step. - -Workflow: - step 1 — change-management-validator (automated: verifies maintenance window) - step 2 — deployment-impact-assessor (automated: assesses blast radius) - step 3 — Change Advisory Board (you) (human: final sign-off, requires_approval) - -Run: - python main.py # interactive scenario picker - SCENARIO=1 python main.py # skip the picker - -Prerequisites: - axme login # one-time; refreshes automatically for 30 days -""" -from __future__ import annotations - -import json -import os -import time -from datetime import datetime, timezone -from pathlib import Path -from typing import Any -from uuid import uuid4 - -from axme import AxmeClient, AxmeClientConfig - - -# --------------------------------------------------------------------------- -# Scenarios -# --------------------------------------------------------------------------- - -SCENARIOS: dict[str, dict[str, Any]] = { - "1": { - "title": "nginx config rollout → prod-cluster-eu", - "summary": "Update nginx config on prod-cluster-eu (change #CHG-4821)", - "scenario_id": "approval.nginx_rollout.v1", - "intent_type": "intent.approval.change_mgmt.v1", - "agents": [ - { - "role": "validator", - "address": "change-management-validator", - "display_name": "Change Management Validator", - "description": "step 1/3 — auto: verifying maintenance window and rollback plan", - }, - { - "role": "assessor", - "address": "deployment-impact-assessor", - "display_name": "Deployment Impact Assessor", - "description": "step 2/3 — auto: assessing blast radius and service dependencies", - }, - ], - "humans": [ - { - "role": "cab", - "display_name": "Change Advisory Board (CAB)", - "description": "step 3/3 — human: final sign-off", - }, - ], - "workflow_steps": [ - { - "step_id": "validate_change", - "tool_id": "tool.approval.check_window", - "assigned_to": "validator", - "step_deadline_seconds": 300, - "label": "change-management-validator", - "description": "verifying maintenance window and rollback plan", - "outcome": "maintenance window confirmed, rollback plan verified", - }, - { - "step_id": "assess_impact", - "tool_id": "tool.approval.risk_assessment", - "assigned_to": "assessor", - "step_deadline_seconds": 300, - "label": "deployment-impact-assessor", - "description": "assessing blast radius and service dependencies", - "outcome": "blast radius: low, zero downtime deployment confirmed", - }, - { - "step_id": "cab_signoff", - "tool_id": "tool.approval.human_signoff", - "assigned_to": "cab", - "requires_approval": True, - "step_deadline_seconds": 3600, - "remind_after_seconds": 300, - "remind_interval_seconds": 300, - "max_reminders": 3, - "human_task": { - "title": "Final CAB sign-off — nginx rollout to prod-cluster-eu", - "description": "Review and approve the nginx config rollout to prod-cluster-eu", - "form_schema": { - "type": "object", - "required": ["approved"], - "properties": { - "approved": {"type": "boolean"}, - "notes": {"type": "string"}, - }, - }, - }, - "label": "Change Advisory Board (CAB)", - "description": "final sign-off required", - }, - ], - "intent_payload": { - "change_id": "CHG-4821", - "service": "nginx", - "environment": "prod-cluster-eu", - "description": "Update nginx config for load balancing improvements", - "risk_level": "medium", - "rollback_plan": "revert to previous nginx.conf via git tag v2.1.1", - }, - "deadline_minutes": 60, - }, - "2": { - "title": "$47,500 cloud infrastructure budget — Q2 expansion", - "summary": "Budget approval: $47,500 cloud infrastructure Q2 expansion (BUD-2024-Q2-EU)", - "scenario_id": "approval.budget_request.v1", - "intent_type": "intent.approval.finance.v1", - "agents": [ - { - "role": "requester", - "address": "budget-request-agent", - "display_name": "Budget Request Agent", - }, - { - "role": "envelope_validator", - "address": "budget-envelope-validator", - "display_name": "Budget Envelope Validator", - "description": "step 1/3 — auto: validating budget envelope against Q2 allocation", - }, - { - "role": "cost_estimator", - "address": "vendor-cost-estimator", - "display_name": "Vendor Cost Estimator", - "description": "step 2/3 — auto: cross-checking vendor quotes and 12-month TCO", - }, - ], - "humans": [ - { - "role": "cfo", - "display_name": "CFO / Finance Committee", - "description": "step 3/3 — human: final budget approval", - }, - ], - "workflow_steps": [ - { - "step_id": "validate_envelope", - "tool_id": "tool.approval.check_window", - "assigned_to": "envelope_validator", - "step_deadline_seconds": 300, - "label": "budget-envelope-validator", - "description": "validating budget envelope against Q2 allocation", - "outcome": "within Q2 envelope, 12% headroom remaining", - }, - { - "step_id": "estimate_cost", - "tool_id": "tool.approval.risk_assessment", - "assigned_to": "cost_estimator", - "step_deadline_seconds": 300, - "label": "vendor-cost-estimator", - "description": "cross-checking vendor quotes and 12-month TCO", - "outcome": "3 vendor quotes validated, TCO within 5% of estimate", - }, - { - "step_id": "cfo_approval", - "tool_id": "tool.approval.human_signoff", - "assigned_to": "cfo", - "requires_approval": True, - "step_deadline_seconds": 86400, - "remind_after_seconds": 3600, - "remind_interval_seconds": 3600, - "max_reminders": 2, - "human_task": { - "title": "Budget approval — Q2 cloud infrastructure $47,500", - "description": "Approve the Q2 cloud expansion budget within allocated envelope", - "form_schema": { - "type": "object", - "required": ["approved"], - "properties": { - "approved": {"type": "boolean"}, - "notes": {"type": "string"}, - }, - }, - }, - "label": "CFO / Finance Committee", - "description": "final budget sign-off required", - }, - ], - "intent_payload": { - "budget_id": "BUD-2024-Q2-EU", - "amount": 47500, - "currency": "USD", - "category": "cloud_infrastructure", - "description": "Q2 cloud expansion — EU region capacity increase", - }, - "deadline_minutes": 1440, - }, - "3": { - "title": "READ access to prod-db-eu-west-1 for svc:data-pipeline", - "summary": "Access request: READ on prod-db-eu-west-1 for svc:data-pipeline (ITSM-ACCESS-8821)", - "scenario_id": "approval.access_request.v1", - "intent_type": "intent.approval.access_mgmt.v1", - "agents": [ - { - "role": "requester", - "address": "access-request-agent", - "display_name": "Access Request Agent", - }, - { - "role": "policy_checker", - "address": "access-policy-checker", - "display_name": "Access Policy Checker", - "description": "step 1/3 — auto: verifying service identity and least-privilege policy", - }, - { - "role": "risk_assessor", - "address": "data-risk-assessor", - "display_name": "Data Risk Assessor", - "description": "step 2/3 — auto: evaluating data sensitivity and audit trail coverage", - }, - ], - "humans": [ - { - "role": "security_officer", - "display_name": "Security Officer / DBA", - "description": "step 3/3 — human: access grant sign-off", - }, - ], - "workflow_steps": [ - { - "step_id": "check_policy", - "tool_id": "tool.approval.check_window", - "assigned_to": "policy_checker", - "step_deadline_seconds": 300, - "label": "access-policy-checker", - "description": "verifying service identity and least-privilege policy", - "outcome": "service identity verified, READ-only scope within policy", - }, - { - "step_id": "assess_risk", - "tool_id": "tool.approval.risk_assessment", - "assigned_to": "risk_assessor", - "step_deadline_seconds": 300, - "label": "data-risk-assessor", - "description": "evaluating data sensitivity and audit trail coverage", - "outcome": "PII fields excluded, audit logging active on target DB", - }, - { - "step_id": "security_signoff", - "tool_id": "tool.approval.human_signoff", - "assigned_to": "security_officer", - "requires_approval": True, - "step_deadline_seconds": 7200, - "remind_after_seconds": 1800, - "remind_interval_seconds": 1800, - "max_reminders": 2, - "human_task": { - "title": "Access grant approval — READ on prod-db-eu-west-1", - "description": "Grant READ access for svc:data-pipeline to prod-db-eu-west-1", - "form_schema": { - "type": "object", - "required": ["approved"], - "properties": { - "approved": {"type": "boolean"}, - "grant_until": {"type": "string"}, - "notes": {"type": "string"}, - }, - }, - }, - "label": "Security Officer / DBA", - "description": "access grant sign-off required", - }, - ], - "intent_payload": { - "request_id": "ITSM-ACCESS-8821", - "resource": "prod-db-eu-west-1", - "access_type": "READ", - "requestor": "svc:data-pipeline", - "justification": "ETL pipeline requires read access to replicate production data for analytics", - }, - "deadline_minutes": 120, - }, - "4": { - "title": "AI agent action: send contract to Acme Corp ($120k)", - "summary": "AI agent requests permission to send $120k contract to Acme Corp (CONTRACT-AC-2024-001)", - "scenario_id": "approval.contract_send.v1", - "intent_type": "intent.approval.ai_oversight.v1", - "agents": [ - { - "role": "ai_agent", - "address": "ai-action-requester", - "display_name": "AI Action Requester", - }, - { - "role": "terms_validator", - "address": "contract-terms-validator", - "display_name": "Contract Terms Validator", - "description": "step 1/3 — auto: validating contract terms and entity details", - }, - { - "role": "compliance_checker", - "address": "compliance-aml-checker", - "display_name": "Compliance AML Checker", - "description": "step 2/3 — auto: running compliance checks (AML, sanctions)", - }, - ], - "humans": [ - { - "role": "account_executive", - "display_name": "Account Executive / Legal", - "description": "step 3/3 — human: contract send approval", - }, - ], - "workflow_steps": [ - { - "step_id": "validate_terms", - "tool_id": "tool.approval.check_window", - "assigned_to": "terms_validator", - "step_deadline_seconds": 300, - "label": "contract-terms-validator", - "description": "validating contract terms, signatures and entity details", - "outcome": "contract terms valid, entities match CRM records", - }, - { - "step_id": "check_compliance", - "tool_id": "tool.approval.risk_assessment", - "assigned_to": "compliance_checker", - "step_deadline_seconds": 300, - "label": "compliance-aml-checker", - "description": "running compliance checks (AML, sanctions, jurisdiction)", - "outcome": "AML clear, no sanctions hits, jurisdiction confirmed", - }, - { - "step_id": "exec_approval", - "tool_id": "tool.approval.human_signoff", - "assigned_to": "account_executive", - "requires_approval": True, - "step_deadline_seconds": 7200, - "remind_after_seconds": 1800, - "remind_interval_seconds": 1800, - "max_reminders": 3, - "human_task": { - "title": "Contract send approval — Acme Corp $120k", - "description": "Approve AI agent action to send the $120k contract to Acme Corp", - "form_schema": { - "type": "object", - "required": ["approved"], - "properties": { - "approved": {"type": "boolean"}, - "notes": {"type": "string"}, - }, - }, - }, - "label": "Account Executive / Legal", - "description": "contract send sign-off required", - }, - ], - "intent_payload": { - "contract_id": "CONTRACT-AC-2024-001", - "counterparty": "Acme Corp", - "value": 120000, - "currency": "USD", - "action": "send_contract", - "deadline": "2024-12-31", - }, - "deadline_minutes": 120, - }, -} - - -# --------------------------------------------------------------------------- -# Secrets / auth -# --------------------------------------------------------------------------- - -_SECRETS_PATH = Path.home() / ".config" / "axme" / "secrets.json" - - -def _read_cli_secrets(context: str = "default") -> dict[str, str]: - try: - data = json.loads(_SECRETS_PATH.read_text()) - return dict(data.get(context) or data.get("default") or {}) - except Exception: - return {} - - -def _require_api_key() -> str: - value = os.getenv("AXME_API_KEY", "").strip() - if not value: - value = _read_cli_secrets().get("api_key", "").strip() - if not value: - print( - "\n [error] No API key found.\n" - " Run: axme login\n" - " Or: export AXME_API_KEY=\n" - ) - raise SystemExit(1) - return value - - -def _require_human_contact(api_key: str = "", base_url: str = "") -> str: - """Return the actor email for human steps. - - Tries in order: - 1. AXME_USER_EMAIL env var - 2. email field in ~/.config/axme/secrets.json - 3. GET /v1/portal/personal/context (uses actor_token from secrets) - Returns empty string if not available — contact is optional on the server. - """ - env_email = os.getenv("AXME_USER_EMAIL", "").strip() - if env_email: - return env_email - secrets = _read_cli_secrets() - from_secrets = secrets.get("email", "").strip() - if from_secrets: - return from_secrets - # Resolve from server via actor_token - try: - actor_token = secrets.get("actor_token", "").strip() - effective_key = api_key or secrets.get("api_key", "").strip() - effective_url = base_url or secrets.get("base_url", "").strip() or "https://api.cloud.axme.ai" - if actor_token and effective_key: - req = urllib.request.Request( - f"{effective_url}/v1/portal/personal/context", - headers={ - "X-Api-Key": effective_key, - "Authorization": f"Bearer {actor_token}", - }, - method="GET", - ) - with urllib.request.urlopen(req, timeout=8) as resp: - body = json.loads(resp.read()) - email = (body.get("account") or {}).get("email", "").strip() - if email: - return email - except Exception: - pass - return "" - - -def _require_base_url() -> str: - return ( - os.getenv("AXME_BASE_URL", "").strip() - or _read_cli_secrets().get("base_url", "").strip() - or "https://api.cloud.axme.ai" - ) - - -# --------------------------------------------------------------------------- -# Output helpers -# --------------------------------------------------------------------------- - -_COL = 22 - - -def _tag(kind: str, msg: str) -> None: - print(f" [{kind:<20}] {msg}") - - -def _divider() -> None: - print(" " + "─" * 72) - - -def _pause(secs: float) -> None: - time.sleep(secs) - - -def _fmt_status(raw: str, reason: str = "") -> str: - mapping = { - "DELIVERED": "DELIVERED", - "ACKNOWLEDGED": "ACKNOWLEDGED", - "IN_PROGRESS": "IN_PROGRESS", - "WAITING": f"WAITING ({'for human' if 'HUMAN' in reason.upper() else 'for agent'})", - "COMPLETED": "COMPLETED ✓", - "FAILED": "FAILED ✗", - "CANCELED": "CANCELED", - "TIMED_OUT": "TIMED_OUT ✗", - } - return mapping.get(raw, raw) - - -def _fmt_pending_with(pw: dict[str, Any] | None) -> str: - if not pw: - return "" - name = pw.get("name") or pw.get("ref") or "" - return str(name).split("/")[-1] - - -# --------------------------------------------------------------------------- -# Scenario picker -# --------------------------------------------------------------------------- - -def _pick_scenario() -> dict[str, Any]: - env_scenario = os.getenv("SCENARIO", "").strip() - if env_scenario in SCENARIOS: - return SCENARIOS[env_scenario] - - print() - _divider() - print(" Axme — approval workflow example (Model B · ScenarioBundle)") - _divider() - for key, sc in SCENARIOS.items(): - print(f" [{key}] {sc['title']}") - print() - try: - choice = input(" Select scenario [1-4]: ").strip() - except (EOFError, KeyboardInterrupt): - raise SystemExit(0) - if choice not in SCENARIOS: - print(f" Invalid choice: {choice!r}") - raise SystemExit(1) - return SCENARIOS[choice] - - -# --------------------------------------------------------------------------- -# Bundle builder -# --------------------------------------------------------------------------- - -def _build_bundle(scenario: dict[str, Any], human_contact: str) -> dict[str, Any]: - """Convert scenario definition into a ScenarioBundleRequest dict.""" - deadline_iso = ( - datetime.now(tz=timezone.utc) - .replace(microsecond=0) - .__class__( - *(datetime.now(tz=timezone.utc).timetuple()[:6]), - tzinfo=timezone.utc, - ) - ) - # Compute deadline_at = now + deadline_minutes - from datetime import timedelta - deadline_at = ( - datetime.now(tz=timezone.utc) + timedelta(minutes=scenario["deadline_minutes"]) - ).isoformat() - - # Agents - agents = [ - { - "role": a["role"], - "address": a["address"], - "create_if_missing": True, - "display_name": a.get("display_name", a["address"]), - } - for a in scenario["agents"] - ] - - # Humans: inject the logged-in user's contact for human steps - humans = [] - for h in scenario["humans"]: - entry: dict[str, Any] = { - "role": h["role"], - "display_name": h.get("display_name", h["role"]), - } - if human_contact: - entry["contact"] = human_contact - humans.append(entry) - - # Workflow steps (strip example-only fields: label, description, outcome) - _example_only = {"label", "description", "outcome"} - workflow_steps = [ - {k: v for k, v in step.items() if k not in _example_only} - for step in scenario["workflow_steps"] - ] - - return { - "scenario_id": scenario["scenario_id"], - "description": scenario["summary"], - "agents": agents, - "humans": humans, - "workflow": {"steps": workflow_steps}, - "intent": { - "type": scenario["intent_type"], - "payload": scenario["intent_payload"], - "deadline_at": deadline_at, - }, - } - - -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- - -def main() -> None: - scenario = _pick_scenario() - - api_key = _require_api_key() - base_url = _require_base_url() - human_contact = _require_human_contact(api_key=api_key, base_url=base_url) - - client = AxmeClient( - AxmeClientConfig( - api_key=api_key, - base_url=base_url, - ) - ) - - n_steps = len(scenario["workflow_steps"]) - human_role = scenario["humans"][0]["display_name"] - - # ── Print scenario overview ─────────────────────────────────────────── - print() - _divider() - print(f" [scenario] {scenario['title']}") - _divider() - print(f" [summary] {scenario['summary']}") - print() - - for i, step in enumerate(scenario["workflow_steps"], 1): - is_human = step.get("requires_approval", False) - kind = "human" if is_human else "agent" - label = step.get("label") or step.get("assigned_to") or step["step_id"] - desc = step.get("description", "") - print(f" [{kind}:step] step {i}/{n_steps} — {label} ({desc})") - print() - - print(f" [system] deadline: {scenario['deadline_minutes']} min from now") - print() - - # ── Apply scenario (one call — server does everything) ──────────────── - _tag("system", "resolving org and workspace …") - _pause(0.2) - - bundle = _build_bundle(scenario, human_contact) - idempotency_key = str(uuid4()) - - _tag("system", - f"provisioning {len(scenario['agents'])} SA agent(s) + " - f"{len(scenario['humans'])} human role(s) via ScenarioBundle …") - _pause(0.3) - - try: - apply_resp = client.apply_scenario(bundle, idempotency_key=idempotency_key) - except Exception as exc: - _tag("error", f"apply_scenario failed: {exc}") - raise SystemExit(1) - - intent_id = str(apply_resp.get("intent_id", "")) - compile_id = str(apply_resp.get("compile_id") or "") - - if not intent_id: - _tag("error", "server returned no intent_id") - raise SystemExit(1) - - for ag in apply_resp.get("agents_provisioned") or []: - _tag("agent:create", f"agent:{{{ag}}}") - _pause(0.3) - - for step_spec in scenario["agents"]: - _tag("agent:assign", - f"agent:{{{step_spec['address']}}} AS {step_spec.get('description', step_spec['role'])}") - _tag("human:assign", - f"human:{{{human_role}}} AS {scenario['humans'][0].get('description', 'final sign-off (you)')}") - print() - - _tag("intent:create", f"intent_id={intent_id}") - if compile_id: - _tag("intent:create", f"workflow compile_id={compile_id}") - _tag("status:change", "— → DELIVERED") - print() - - # ── Observe event stream ────────────────────────────────────────────── - # Track display state - cur_status = "DELIVERED" - cur_holder = scenario["agents"][0]["address"] - step_counter = 0 - - _TERMINAL = {"COMPLETED", "FAILED", "CANCELED", "TIMED_OUT"} - - try: - for event in client.observe(intent_id, since=0, timeout_seconds=600): - ev_status = str(event.get("status") or "") - ev_reason = str(event.get("lifecycle_waiting_reason") or event.get("reason") or "") - ev_type = str(event.get("event_type") or "") - raw_pw = event.get("pending_with") - new_holder = _fmt_pending_with(raw_pw) if isinstance(raw_pw, dict) else cur_holder - - # ── human_task_assigned: pause and wait for Enter ───────────── - if ev_type == "intent.human_task_assigned": - ht = event.get("human_task") or {} - title = ht.get("title") or f"{human_role} approval" - schema = ht.get("form_schema") - - step_counter += 1 - print() - _divider() - print(f" ── step {n_steps}/{n_steps} 👤 {human_role} approval") - _divider() - _tag("system", f"task: {title}") - _tag("status:change", f"{cur_status} → {_fmt_status('WAITING', 'WAITING_FOR_HUMAN')}") - _tag("cur_holder:change", - f"{cur_holder} → {new_holder or human_role}") - - cur_status = _fmt_status("WAITING", "WAITING_FOR_HUMAN") - cur_holder = new_holder or human_role - - if schema: - fields = list((schema.get("properties") or {}).keys()) - _tag("system", f"form fields: {', '.join(fields)}") - print() - _tag("system", f"You are acting as: {human_role}") - _tag("system", "Press Enter to approve, or Ctrl+C to cancel.") - print() - - try: - input(" > ") - except (EOFError, KeyboardInterrupt): - print("\n [cancelled] approval cancelled.") - raise SystemExit(0) - - print() - _tag("action:approve", f"{human_role} approved") - _pause(0.3) - - # Submit approval back to server - try: - client.resume_intent( - intent_id, - { - "action": "approve", - "task_result": {"approved": True}, - }, - ) - except Exception as exc: - _tag("error", f"resume_intent (human) failed: {exc}") - raise - - continue - - # ── Normal lifecycle status events ──────────────────────────── - if not ev_status: - continue - - new_fmt = _fmt_status(ev_status, ev_reason) - - # Step header when holder changes or step advances - if new_holder and new_holder != cur_holder: - # Figure out which step we're on - for i, step in enumerate(scenario["workflow_steps"], 1): - step_label = step.get("label") or step.get("assigned_to") or step["step_id"] - if new_holder in step_label or step_label in new_holder: - step_counter = i - break - if step_counter > 0 and not scenario["workflow_steps"][step_counter - 1].get("requires_approval"): - step = scenario["workflow_steps"][step_counter - 1] - label = step.get("label") or step.get("assigned_to") or step["step_id"] - desc = step.get("description", "") - print() - _divider() - print(f" ── step {step_counter}/{n_steps} ⚙ {label} approval") - _divider() - _tag("system", f"task: {desc}") - - if new_fmt != cur_status: - _tag("status:change", f"{cur_status} → {new_fmt}") - _pause(0.3) - - if new_holder and new_holder != cur_holder: - _tag("cur_holder:change", f"{cur_holder} → {new_holder}") - - # If automated step: print approval outcome after a brief delay - for step in scenario["workflow_steps"]: - if not step.get("requires_approval"): - step_label = step.get("label") or step.get("assigned_to") or step["step_id"] - if cur_holder in step_label or step_label in cur_holder: - outcome = step.get("outcome") - if outcome: - _pause(0.5) - _tag("action:approve", f"{step_label} approved — {outcome}") - break - - _pause(0.4) - - cur_status = new_fmt - cur_holder = new_holder or cur_holder - - if ev_status in _TERMINAL: - break - - except TimeoutError: - _tag("error", "observation timed out after 10 min — check intent status manually") - _tag("system", f"axme intents get {intent_id}") - raise SystemExit(1) - - # ── Final summary ───────────────────────────────────────────────────── - print() - _divider() - print() - print(f" Result:") - print(f" Scenario: {scenario['title']}") - print(f" Intent ID: {intent_id}") - print(f" Final status: {cur_status}") - if compile_id: - print(f" Workflow: compile_id={compile_id}") - print() - print(f" Verify via CLI:") - print(f" axme intents get {intent_id}") - print(f" axme intents watch {intent_id}") - print(f" axme agents list") - print(f" axme quota show") - print() - - # ── Full event history from server ──────────────────────────────────── - _divider() - print() - print(" Intent event log (server audit trail):") - print() - try: - events_resp = client.list_intent_events(intent_id) - events = events_resp.get("events") or [] - if events: - col_w = [5, 21, 28, 30, 28] - header = ( - f" {'#':<{col_w[0]}}" - f"{'time (UTC)':<{col_w[1]}}" - f"{'status':<{col_w[2]}}" - f"{'actor':<{col_w[3]}}" - f"{'cur_holder (pending_with)':<{col_w[4]}}" - ) - print(header) - print(" " + "─" * (sum(col_w) + 2)) - for ev in events: - seq = str(ev.get("seq", "")) - at = str(ev.get("at", ""))[:19].replace("T", " ") - status = str(ev.get("status", "")) - actor = str(ev.get("actor") or "").split("/")[-1][:28] - pw = ev.get("pending_with") or {} - holder = str( - pw.get("name") or pw.get("ref") or "" - ).split("/")[-1][:26] if pw else "" - reason = str( - (ev.get("details") or {}).get("reason") or - (ev.get("details") or {}).get("next_handler") or "" - ) - status_col = f"{status} ({reason})" if reason else status - print( - f" {seq:<{col_w[0]}}" - f"{at:<{col_w[1]}}" - f"{status_col:<{col_w[2]}}" - f"{actor:<{col_w[3]}}" - f"{holder:<{col_w[4]}}" - ) - print() - else: - print(" (no events returned)") - print() - except Exception as exc: - print(f" (could not fetch event log: {exc})") - print() - - -if __name__ == "__main__": - main() diff --git a/examples/approval-workflow/python/requirements.txt b/examples/approval-workflow/python/requirements.txt deleted file mode 100644 index e27c78b..0000000 --- a/examples/approval-workflow/python/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -axme>=0.1.1 -python-dotenv>=1.0.1 diff --git a/examples/approval-workflow/typescript/main.ts b/examples/approval-workflow/typescript/main.ts deleted file mode 100644 index fc384f7..0000000 --- a/examples/approval-workflow/typescript/main.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { config as loadEnv } from "dotenv"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { AxmeClient } from "@axme/axme/dist/src/index.js"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -loadEnv({ path: path.resolve(__dirname, "..", ".env") }); - -function requireEnv(name: string): string { - const value = (process.env[name] ?? "").trim(); - if (!value) { - throw new Error( - `${name} is not set. Run 'axme login' to sign in, then:\n` + - ` export ${name}=$(axme context show --show-key --json | jq -r .api_key)` - ); - } - return value; -} - -function printEvents(events: Array>): void { - for (const event of events) { - const seq = typeof event.seq === "number" ? event.seq : 0; - const eventType = String(event.event_type ?? "unknown"); - const status = String(event.status ?? "unknown"); - const waitingReason = event.waiting_reason ? ` waiting_reason=${String(event.waiting_reason)}` : ""; - console.log(`[event] seq=${seq} type=${eventType} status=${status}${waitingReason}`); - } -} - -async function main(): Promise { - const baseUrl = (process.env.AXME_BASE_URL ?? "https://api.cloud.axme.ai").trim(); - const apiKey = requireEnv("AXME_API_KEY"); - const actorToken = (process.env.AXME_ACTOR_TOKEN ?? "").trim() || undefined; - const fromAgent = (process.env.AXME_FROM_AGENT ?? "agent://examples/requester").trim(); - const toAgent = (process.env.AXME_TO_AGENT ?? "agent://examples/approver").trim(); - const ownerAgent = (process.env.AXME_APPROVAL_OWNER_AGENT ?? fromAgent).trim(); - - const client = new AxmeClient({ - baseUrl, - apiKey, - actorToken, - }); - - const correlationId = crypto.randomUUID(); - const idempotencyKey = `approval-${correlationId}`; - const payload = { - intent_type: "intent.approval.demo.v1", - correlation_id: correlationId, - from_agent: fromAgent, - to_agent: toAgent, - payload: { - request_id: `req-${correlationId.slice(0, 8)}`, - summary: "Auto-approved rollout request.", - requested_by: fromAgent, - approval_mode: "automatic", - }, - }; - - const created = await client.createIntent(payload, { - correlationId, - idempotencyKey, - }); - const intentId = String(created.intent_id); - console.log(`[create] intent_id=${intentId} status=${String(created.status ?? "unknown")}`); - console.log("[approval] auto-approval path enabled; no manual waiting."); - - const resumed = await client.resumeIntent( - intentId, - { - approve_current_step: true, - reason: "auto-approved by policy", - }, - { ownerAgent }, - ); - console.log( - `[resume] applied=${String(resumed.applied ?? "unknown")} policy_generation=${String(resumed.policy_generation ?? "unknown")}`, - ); - - const resolved = await client.resolveIntent(intentId, { - status: "COMPLETED", - result: { - approval_result: "auto-approved", - approval_mode: "automatic", - }, - }); - const terminalEvent = (resolved.event ?? {}) as Record; - console.log(`[resolve] terminal_type=${String(terminalEvent.event_type ?? "unknown")} status=${String(terminalEvent.status ?? "unknown")}`); - - const listed = await client.listIntentEvents(intentId); - const events = Array.isArray(listed.events) - ? listed.events.filter((item): item is Record => Boolean(item) && typeof item === "object") - : []; - printEvents(events); - - const finalIntent = ((await client.getIntent(intentId)).intent ?? {}) as Record; - console.log( - `[final] intent_id=${intentId} status=${String(finalIntent.status ?? "unknown")} lifecycle_status=${String(finalIntent.lifecycle_status ?? "unknown")}`, - ); -} - -main().catch((error) => { - console.error("[error]", error); - process.exit(1); -}); diff --git a/examples/approval-workflow/typescript/package-lock.json b/examples/approval-workflow/typescript/package-lock.json deleted file mode 100644 index 8a9cea2..0000000 --- a/examples/approval-workflow/typescript/package-lock.json +++ /dev/null @@ -1,160 +0,0 @@ -{ - "name": "axme-example-approval-workflow-ts", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "axme-example-approval-workflow-ts", - "dependencies": { - "@axme/axme": "^0.1.1", - "dotenv": "^16.4.5" - }, - "devDependencies": { - "@types/node": "^25.3.5", - "tsx": "^4.20.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@axme/axme": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@axme/axme/-/axme-0.1.1.tgz", - "integrity": "sha512-mbUKRWx1n37yyivGaMLS/z7+WF6Pnl//LvCCJCVQCpQ3+yO5yOJ4FafGWjqTb0AKkDmdbD1bytJSameQYfn34Q==", - "engines": { - "node": ">=20" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@types/node": { - "version": "25.3.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", - "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, - "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, - "license": "MIT" - } - } -} diff --git a/examples/approval-workflow/typescript/package.json b/examples/approval-workflow/typescript/package.json deleted file mode 100644 index e425833..0000000 --- a/examples/approval-workflow/typescript/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "axme-example-approval-workflow-ts", - "private": true, - "type": "module", - "scripts": { - "start": "tsx main.ts" - }, - "dependencies": { - "@axme/axme": "^0.1.1", - "dotenv": "^16.4.5" - }, - "devDependencies": { - "@types/node": "^25.3.5", - "tsx": "^4.20.0" - }, - "engines": { - "node": ">=20" - } -} diff --git a/examples/approval-workflow/typescript/tsconfig.json b/examples/approval-workflow/typescript/tsconfig.json deleted file mode 100644 index 32683f5..0000000 --- a/examples/approval-workflow/typescript/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": [ - "node" - ], - "lib": [ - "ES2022" - ], - "noEmit": true, - "skipLibCheck": true - }, - "include": [ - "main.ts" - ] -} diff --git a/examples/delivery/http/dotnet/HttpAgent.cs b/examples/delivery/http/dotnet/HttpAgent.cs new file mode 100644 index 0000000..9163dec --- /dev/null +++ b/examples/delivery/http/dotnet/HttpAgent.cs @@ -0,0 +1,71 @@ +using System.Net; +using System.Text.Json; +using System.Text.Json.Nodes; +using Axme.Sdk; +using AxmeExamples; + +namespace AxmeExamples.Delivery; + +public class HttpAgent +{ + public static async Task Main(string[] args) + { + var baseUrl = SseHelper.Env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + var apiKey = SseHelper.Env("AXME_API_KEY"); + var addr = SseHelper.Env("AXME_AGENT_ADDRESS", "http-order-processor"); + var port = int.Parse(SseHelper.Env("PORT", "8080")); + if (string.IsNullOrEmpty(apiKey)) { Console.Error.WriteLine("AXME_API_KEY is required"); return; } + + var client = new AxmeClient(new AxmeClientConfig { ApiKey = apiKey, BaseUrl = baseUrl }); + string[] currencies = ["USD", "EUR", "GBP", "JPY"]; + + var listener = new HttpListener(); + listener.Prefixes.Add($"http://+:{port}/"); + listener.Start(); + Console.WriteLine($"http-order-processor starting port={port}"); + + while (true) + { + var ctx = await listener.GetContextAsync(); + if (ctx.Request.Url?.AbsolutePath == "/health") + { + ctx.Response.StatusCode = 200; + await ctx.Response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes("{\"status\":\"ok\"}")); + ctx.Response.Close(); continue; + } + if (ctx.Request.HttpMethod != "POST" || ctx.Request.Url?.AbsolutePath != "/intent") + { + ctx.Response.StatusCode = 404; ctx.Response.Close(); continue; + } + using var reader = new StreamReader(ctx.Request.InputStream); + var body = await reader.ReadToEndAsync(); + ctx.Response.StatusCode = 200; + await ctx.Response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes("{\"ack\":true}")); + ctx.Response.Close(); + + var envelope = JsonNode.Parse(body)?.AsObject(); + var intentId = SseHelper.Str(envelope ?? new JsonObject(), "intent_id"); + if (!string.IsNullOrEmpty(intentId)) + { + _ = Task.Run(async () => + { + try + { + var intent = await client.GetIntentAsync(intentId); + var p = intent["payload"]?.AsObject() ?? new JsonObject(); + var orderId = SseHelper.Str(p, "order_id"); + var currency = SseHelper.Str(p, "currency"); + JsonObject res; + if (SseHelper.Str(p, "event_type") != "order.placed") res = new JsonObject { ["action"] = "fail", ["reason"] = "unexpected event_type" }; + else if (string.IsNullOrEmpty(orderId)) res = new JsonObject { ["action"] = "fail", ["reason"] = "order_id required" }; + else if (!currencies.Contains(currency)) res = new JsonObject { ["action"] = "fail", ["reason"] = "unsupported currency" }; + else res = new JsonObject { ["action"] = "complete", ["order_id"] = orderId, ["tracking_id"] = $"TRK-{orderId.ToUpper()}-{Math.Abs(orderId.GetHashCode()) % 100000:D5}", ["status"] = "accepted" }; + await client.ResumeIntentAsync(intentId, res, new RequestOptions { OwnerAgent = addr }); + Console.WriteLine($"resumed {intentId}"); + } + catch (Exception ex) { Console.Error.WriteLine($"error: {ex.Message}"); } + }); + } + } + } +} diff --git a/examples/delivery/http/go/agent.go b/examples/delivery/http/go/agent.go new file mode 100644 index 0000000..ebdaaa7 --- /dev/null +++ b/examples/delivery/http/go/agent.go @@ -0,0 +1,151 @@ +// Order Processing Agent — HTTP delivery binding (Go). +// +// Run: +// +// export AXME_API_KEY= +// export CALLBACK_URL=https:///intent +// export PORT=8080 +// go run examples/delivery/http/agent.go +package main + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "log" + "math" + "net/http" + "os" + "strings" + + "github.com/AxmeAI/axme-sdk-go/axme" +) + +var ( + baseURL = envOr("AXME_BASE_URL", "https://api.cloud.axme.ai") + apiKey = os.Getenv("AXME_API_KEY") + agentAddress = envOr("AXME_AGENT_ADDRESS", "http-order-processor") + callbackURL = os.Getenv("CALLBACK_URL") + port = envOr("PORT", "8080") + webhookSecret = os.Getenv("AXME_WEBHOOK_SECRET") +) + +var client *axme.Client + +func envOr(k, d string) string { + if v := os.Getenv(k); v != "" { return v } + return d +} + +func str(m map[string]any, k, d string) string { + if v, ok := m[k].(string); ok { return v } + return d +} + +var supportedCurrencies = map[string]bool{"USD": true, "EUR": true, "GBP": true, "JPY": true} + +func processOrder(p map[string]any) map[string]any { + eventType := str(p, "event_type", "") + orderID := str(p, "order_id", "") + amount, _ := p["amount"].(float64) + currency := str(p, "currency", "") + + if eventType != "order.placed" { + return map[string]any{"action": "fail", "reason": fmt.Sprintf("unexpected event_type=%q", eventType)} + } + if orderID == "" { + return map[string]any{"action": "fail", "reason": "order_id is required"} + } + if !supportedCurrencies[currency] { + return map[string]any{"action": "fail", "reason": fmt.Sprintf("unsupported currency=%q", currency)} + } + if amount < 0.01 || amount > 1_000_000 { + return map[string]any{"action": "fail", "reason": fmt.Sprintf("amount=%v out of range", amount)} + } + + h := int(math.Abs(float64(hashCode(orderID)))) % 100000 + trackingID := fmt.Sprintf("TRK-%s-%05d", strings.ToUpper(orderID), h) + return map[string]any{ + "action": "complete", "order_id": orderID, "tracking_id": trackingID, + "status": "accepted", "currency": currency, "amount": amount, + } +} + +func hashCode(s string) int { + h := 0 + for _, c := range s { + h = 31*h + int(c) + } + return h +} + +func verifySignature(body []byte, sig string) bool { + if webhookSecret == "" { return true } + if sig == "" { return false } + mac := hmac.New(sha256.New, []byte(webhookSecret)) + mac.Write(body) + expected := hex.EncodeToString(mac.Sum(nil)) + received := strings.TrimPrefix(sig, "sha256=") + return hmac.Equal([]byte(expected), []byte(received)) +} + +func handleIntentAsync(intentID string) { + ctx := context.Background() + log.Printf("received intent %s via http callback", intentID) + + resp, err := client.GetIntent(ctx, intentID, axme.RequestOptions{}) + if err != nil { log.Printf("get_intent(%s) failed: %v", intentID, err); return } + + payload, _ := resp["payload"].(map[string]any) + if payload == nil { payload = map[string]any{} } + + result := processOrder(payload) + action := str(result, "action", "") + + _, err = client.ResumeIntent(ctx, intentID, result, axme.RequestOptions{OwnerAgent: agentAddress}) + if err != nil { log.Printf("resume_intent(%s) failed: %v", intentID, err); return } + log.Printf("resumed %s action=%s", intentID, action) +} + +func main() { + if apiKey == "" { log.Fatal("AXME_API_KEY is required") } + + var err error + client, err = axme.NewClient(axme.ClientConfig{APIKey: apiKey, BaseURL: baseURL}) + if err != nil { log.Fatalf("client init: %v", err) } + + http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"status":"ok"}`)) + }) + + http.HandleFunc("/intent", func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { w.WriteHeader(405); return } + body, _ := io.ReadAll(r.Body) + + if !verifySignature(body, r.Header.Get("X-Axme-Signature")) { + w.WriteHeader(401) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"ack":true}`)) + + var envelope map[string]any + if err := json.Unmarshal(body, &envelope); err != nil { return } + intentID := str(envelope, "intent_id", "") + if intentID != "" { + go handleIntentAsync(intentID) + } + }) + + log.Printf("http-order-processor starting address=%s binding=http port=%s", agentAddress, port) + if callbackURL != "" { + log.Printf("AXME will POST intents to: %s", callbackURL) + } + log.Fatal(http.ListenAndServe(":"+port, nil)) +} diff --git a/examples/delivery/http/java/HttpAgent.java b/examples/delivery/http/java/HttpAgent.java new file mode 100644 index 0000000..0f1095a --- /dev/null +++ b/examples/delivery/http/java/HttpAgent.java @@ -0,0 +1,62 @@ +package ai.axme.examples.delivery; + +import ai.axme.examples.SseHelper; +import dev.axme.sdk.*; +import com.sun.net.httpserver.*; + +import java.io.*; +import java.net.InetSocketAddress; +import java.util.*; + +/** Order Processing Agent — HTTP delivery binding (Java). */ +public class HttpAgent { + static final String BASE = SseHelper.env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + static final String KEY = System.getenv("AXME_API_KEY"); + static final String ADDR = SseHelper.env("AXME_AGENT_ADDRESS", "http-order-processor"); + static final int PORT = Integer.parseInt(SseHelper.env("PORT", "8080")); + static final Set CURRENCIES = Set.of("USD", "EUR", "GBP", "JPY"); + + static AxmeClient client; + + public static void main(String[] args) throws Exception { + if (KEY == null || KEY.isEmpty()) { System.err.println("AXME_API_KEY is required"); System.exit(1); } + client = new AxmeClient(new AxmeClientConfig(BASE, KEY)); + + HttpServer server = HttpServer.create(new InetSocketAddress(PORT), 0); + server.createContext("/health", ex -> { ex.sendResponseHeaders(200, 15); ex.getResponseBody().write("{\"status\":\"ok\"}".getBytes()); ex.close(); }); + server.createContext("/intent", ex -> { + if (!"POST".equals(ex.getRequestMethod())) { ex.sendResponseHeaders(405, -1); ex.close(); return; } + byte[] body = ex.getRequestBody().readAllBytes(); + ex.sendResponseHeaders(200, 10); ex.getResponseBody().write("{\"ack\":true}".getBytes()); ex.close(); + Map envelope = SseHelper.parseJson(new String(body)); + String id = SseHelper.str(envelope, "intent_id"); + if (!id.isEmpty()) new Thread(() -> handleAsync(id)).start(); + }); + server.start(); + System.out.printf("http-order-processor starting port=%d%n", PORT); + } + + static void handleAsync(String id) { + try { + Map intent = client.getIntent(id, RequestOptions.none()); + @SuppressWarnings("unchecked") + Map p = (Map) intent.getOrDefault("payload", Map.of()); + String eventType = SseHelper.str(p, "event_type"); + String orderId = SseHelper.str(p, "order_id"); + double amount = p.get("amount") instanceof Number n ? n.doubleValue() : 0; + String currency = SseHelper.str(p, "currency"); + + Map res = new HashMap<>(); + if (!"order.placed".equals(eventType)) { res.put("action", "fail"); res.put("reason", "unexpected event_type"); } + else if (orderId.isEmpty()) { res.put("action", "fail"); res.put("reason", "order_id is required"); } + else if (!CURRENCIES.contains(currency)) { res.put("action", "fail"); res.put("reason", "unsupported currency"); } + else { + res.put("action", "complete"); res.put("order_id", orderId); + res.put("tracking_id", "TRK-" + orderId.toUpperCase() + "-" + String.format("%05d", Math.abs(orderId.hashCode()) % 100000)); + res.put("status", "accepted"); res.put("currency", currency); res.put("amount", amount); + } + client.resumeIntent(id, res, SseHelper.withOwner(ADDR)); + System.out.printf("resumed %s action=%s%n", id, res.get("action")); + } catch (Exception e) { System.err.println("handle error: " + e.getMessage()); } + } +} diff --git a/examples/delivery/http/agent.py b/examples/delivery/http/python/agent.py similarity index 100% rename from examples/delivery/http/agent.py rename to examples/delivery/http/python/agent.py diff --git a/examples/delivery/http/typescript/agent.ts b/examples/delivery/http/typescript/agent.ts new file mode 100644 index 0000000..fde9bb0 --- /dev/null +++ b/examples/delivery/http/typescript/agent.ts @@ -0,0 +1,110 @@ +/** + * Order Processing Agent — HTTP delivery binding (TypeScript). + * + * Delivery: http (AXME POSTs intent to callback_url) + * The agent runs an HTTP server. AXME delivers intents by POSTing to the callback URL. + * + * Run: + * export AXME_API_KEY= + * export CALLBACK_URL=https:///intent + * export PORT=8080 + * npx tsx examples/delivery/http/agent.ts + */ +import { createServer } from "node:http"; +import { createHmac, timingSafeEqual } from "node:crypto"; +import { AxmeClient } from "@axme/axme"; + +const AXME_BASE_URL = process.env.AXME_BASE_URL ?? "https://api.cloud.axme.ai"; +const AXME_API_KEY = process.env.AXME_API_KEY ?? ""; +const AXME_AGENT_ADDRESS = process.env.AXME_AGENT_ADDRESS ?? "http-order-processor"; +const CALLBACK_URL = process.env.CALLBACK_URL ?? ""; +const PORT = parseInt(process.env.PORT ?? "8080", 10); +const AXME_WEBHOOK_SECRET = process.env.AXME_WEBHOOK_SECRET ?? ""; + +if (!AXME_API_KEY) { console.error("AXME_API_KEY is required"); process.exit(1); } + +const client = new AxmeClient({ apiKey: AXME_API_KEY, baseUrl: AXME_BASE_URL }); + +// --- Business logic --- +const SUPPORTED_CURRENCIES = new Set(["USD", "EUR", "GBP", "JPY"]); + +function processOrder(payload: Record): Record { + const eventType = String(payload.event_type ?? ""); + const orderId = String(payload.order_id ?? ""); + const amount = Number(payload.amount ?? 0); + const currency = String(payload.currency ?? ""); + + if (eventType !== "order.placed") return { action: "fail", reason: `unexpected event_type='${eventType}'` }; + if (!orderId) return { action: "fail", reason: "order_id is required" }; + if (!SUPPORTED_CURRENCIES.has(currency)) return { action: "fail", reason: `unsupported currency='${currency}'` }; + if (amount < 0.01 || amount > 1_000_000) return { action: "fail", reason: `amount=${amount} out of range` }; + + const trackingId = `TRK-${orderId.toUpperCase()}-${String(Math.abs(hashCode(orderId)) % 100000).padStart(5, "0")}`; + return { action: "complete", order_id: orderId, tracking_id: trackingId, status: "accepted", currency, amount }; +} + +function hashCode(s: string): number { + let h = 0; + for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0; + return h; +} + +// --- HMAC verification --- +function verifySignature(body: Buffer, sig: string): boolean { + if (!AXME_WEBHOOK_SECRET) return true; + if (!sig) return false; + const expected = createHmac("sha256", AXME_WEBHOOK_SECRET).update(body).digest("hex"); + const received = sig.replace("sha256=", ""); + try { + return timingSafeEqual(Buffer.from(expected), Buffer.from(received)); + } catch { return false; } +} + +// --- HTTP server --- +async function handleIntentAsync(intentId: string): Promise { + console.log(`received intent ${intentId} via http callback`); + let intent: Record; + try { intent = await client.getIntent(intentId); } catch (e) { console.error(`get_intent(${intentId}) failed:`, e); return; } + + const payload = (intent.payload ?? {}) as Record; + const result = processOrder(payload); + const action = result.action; + delete result.action; + + try { + await client.resumeIntent(intentId, { action, ...result }, { ownerAgent: AXME_AGENT_ADDRESS }); + console.log(`resumed ${intentId} action=${action}`); + } catch (e) { console.error(`resume_intent(${intentId}) failed:`, e); } +} + +const server = createServer((req, res) => { + if (req.method === "GET" && req.url === "/health") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end('{"status":"ok"}'); + return; + } + if (req.method !== "POST" || req.url !== "/intent") { + res.writeHead(404); res.end(); return; + } + + const chunks: Buffer[] = []; + req.on("data", (c: Buffer) => chunks.push(c)); + req.on("end", () => { + const body = Buffer.concat(chunks); + if (!verifySignature(body, String(req.headers["x-axme-signature"] ?? ""))) { + res.writeHead(401); res.end(); return; + } + res.writeHead(200, { "Content-Type": "application/json" }); + res.end('{"ack":true}'); + + try { + const envelope = JSON.parse(body.toString()) as Record; + const intentId = String(envelope.intent_id ?? ""); + if (intentId) handleIntentAsync(intentId).catch(e => console.error("async handler error:", e)); + } catch (e) { console.error("failed to parse delivery body:", e); } + }); +}); + +console.log(`http-order-processor starting address=${AXME_AGENT_ADDRESS} binding=http port=${PORT}`); +console.log(`AXME will POST intents to: ${CALLBACK_URL || `http://localhost:${PORT}/intent`}`); +server.listen(PORT); diff --git a/examples/delivery/inbox/dotnet/InboxAgent.cs b/examples/delivery/inbox/dotnet/InboxAgent.cs new file mode 100644 index 0000000..5320eba --- /dev/null +++ b/examples/delivery/inbox/dotnet/InboxAgent.cs @@ -0,0 +1,53 @@ +using System.Text.Json.Nodes; +using Axme.Sdk; +using AxmeExamples; + +namespace AxmeExamples.Delivery; + +public class InboxAgent +{ + public static async Task Main(string[] args) + { + var baseUrl = SseHelper.Env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + var apiKey = SseHelper.Env("AXME_API_KEY"); + var addr = SseHelper.Env("AXME_AGENT_ADDRESS", "fulfillment-service-agent"); + if (string.IsNullOrEmpty(apiKey)) { Console.Error.WriteLine("AXME_API_KEY is required"); return; } + + var client = new AxmeClient(new AxmeClientConfig { ApiKey = apiKey, BaseUrl = baseUrl }); + Console.WriteLine($"fulfillment-service-agent starting address={addr}"); + + var shipping = new Dictionary { ["express"] = "1-2 business days", ["standard"] = "3-5 business days", ["economy"] = "7-10 business days" }; + var since = 0; + + while (true) + { + try + { + foreach (var d in await SseHelper.PollAgentStreamAsync(baseUrl, apiKey, addr, since)) + { + var id = SseHelper.Str(d, "intent_id"); + if (d["seq"]?.GetValue() is int seq) since = Math.Max(since, seq); + if (string.IsNullOrEmpty(id)) continue; + var intent = await client.GetIntentAsync(id); + var p = intent["payload"]?.AsObject() ?? new JsonObject(); + var orderId = SseHelper.Str(p, "order_id"); + var ship = SseHelper.Str(p, "shipping", "standard"); + var items = p["items"]?.AsArray(); + var total = items?.Sum(i => i?["quantity"]?.GetValue() ?? 0) ?? 0; + + JsonObject res; + if (string.IsNullOrEmpty(orderId)) res = new JsonObject { ["action"] = "fail", ["reason"] = "order_id required" }; + else if (items == null || items.Count == 0) res = new JsonObject { ["action"] = "fail", ["reason"] = "items empty" }; + else if (!shipping.ContainsKey(ship)) res = new JsonObject { ["action"] = "fail", ["reason"] = "unknown shipping" }; + else res = new JsonObject { ["action"] = "complete", ["fulfillment_id"] = $"FUL-{Math.Abs((orderId + SseHelper.Str(p, "customer_id")).GetHashCode()) % 100000:D5}", + ["tracking_number"] = $"AXME-{orderId.ToUpper()}-{Math.Abs(orderId.GetHashCode()) % 10000:D4}", + ["warehouse"] = "WH-US-EAST-1", ["shipping_method"] = ship, ["eta"] = shipping[ship], ["items_shipped"] = total, ["status"] = "fulfilled" }; + + await client.ResumeIntentAsync(id, res, new RequestOptions { OwnerAgent = addr }); + Console.WriteLine($"resumed {id} action={res["action"]}"); + } + } + catch { await Task.Delay(2000); } + } + } +} diff --git a/examples/delivery/inbox/go/agent.go b/examples/delivery/inbox/go/agent.go new file mode 100644 index 0000000..3eade67 --- /dev/null +++ b/examples/delivery/inbox/go/agent.go @@ -0,0 +1,103 @@ +// Fulfillment Service Agent — inbox/reply_to delivery binding (Go). +// +// Run: +// +// export AXME_API_KEY= +// export AXME_AGENT_ADDRESS=fulfillment-service-agent +// go run examples/delivery/inbox/agent.go +package main + +import ( + "context" + "fmt" + "log" + "math" + "os" + "os/signal" + "strings" + + "github.com/AxmeAI/axme-sdk-go/axme" +) + +var ( + baseURL = envOr("AXME_BASE_URL", "https://api.cloud.axme.ai") + apiKey = os.Getenv("AXME_API_KEY") + agentAddress = envOr("AXME_AGENT_ADDRESS", "fulfillment-service-agent") +) + +func envOr(k, d string) string { if v := os.Getenv(k); v != "" { return v }; return d } +func str(m map[string]any, k, d string) string { if v, ok := m[k].(string); ok { return v }; return d } + +var shippingEstimates = map[string]string{ + "express": "1-2 business days", "standard": "3-5 business days", "economy": "7-10 business days", +} + +func hashCode(s string) int { h := 0; for _, c := range s { h = 31*h + int(c) }; return h } + +func fulfillOrder(p map[string]any) map[string]any { + orderID := str(p, "order_id", "") + customer := str(p, "customer_id", "") + items, _ := p["items"].([]any) + shipping := str(p, "shipping", "standard") + + if orderID == "" { return map[string]any{"action": "fail", "reason": "order_id is required"} } + if len(items) == 0 { return map[string]any{"action": "fail", "reason": "items list is empty"} } + if _, ok := shippingEstimates[shipping]; !ok { + return map[string]any{"action": "fail", "reason": fmt.Sprintf("unknown shipping method=%q", shipping)} + } + + totalItems := 0 + for _, it := range items { + if m, ok := it.(map[string]any); ok { + if q, ok := m["quantity"].(float64); ok { totalItems += int(q) } + } + } + if totalItems <= 0 { return map[string]any{"action": "fail", "reason": "total item quantity must be > 0"} } + + h1 := int(math.Abs(float64(hashCode(orderID)))) % 10000 + h2 := int(math.Abs(float64(hashCode(orderID + customer)))) % 100000 + return map[string]any{ + "action": "complete", + "fulfillment_id": fmt.Sprintf("FUL-%05d", h2), + "tracking_number": fmt.Sprintf("AXME-%s-%04d", strings.ToUpper(orderID), h1), + "warehouse": "WH-US-EAST-1", "shipping_method": shipping, + "eta": shippingEstimates[shipping], "items_shipped": totalItems, "status": "fulfilled", + } +} + +func main() { + if apiKey == "" { log.Fatal("AXME_API_KEY is required") } + client, err := axme.NewClient(axme.ClientConfig{APIKey: apiKey, BaseURL: baseURL}) + if err != nil { log.Fatalf("client init: %v", err) } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + log.Printf("fulfillment-service-agent starting address=%s binding=stream", agentAddress) + + intents, errs := client.Listen(ctx, agentAddress, axme.ListenOptions{}) + for { + select { + case delivery, ok := <-intents: + if !ok { return } + id := str(delivery, "intent_id", "") + if id == "" { continue } + log.Printf("received order intent %s", id) + + resp, err := client.GetIntent(ctx, id, axme.RequestOptions{}) + if err != nil { log.Printf("get_intent(%s) failed: %v", id, err); continue } + payload, _ := resp["payload"].(map[string]any) + if payload == nil { payload = map[string]any{} } + + result := fulfillOrder(payload) + _, err = client.ResumeIntent(ctx, id, result, axme.RequestOptions{OwnerAgent: agentAddress}) + if err != nil { log.Printf("resume_intent(%s) failed: %v", id, err); continue } + log.Printf("resumed %s action=%s", id, str(result, "action", "")) + case err, ok := <-errs: + if !ok { return } + log.Printf("stream error: %v", err) + case <-ctx.Done(): + log.Println("shutting down"); return + } + } +} diff --git a/examples/delivery/inbox/java/InboxAgent.java b/examples/delivery/inbox/java/InboxAgent.java new file mode 100644 index 0000000..d1add29 --- /dev/null +++ b/examples/delivery/inbox/java/InboxAgent.java @@ -0,0 +1,47 @@ +package ai.axme.examples.delivery; + +import ai.axme.examples.SseHelper; +import dev.axme.sdk.*; +import java.util.*; + +/** Fulfillment Service Agent — inbox/reply_to delivery (Java). */ +public class InboxAgent { + static final String BASE = SseHelper.env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + static final String KEY = System.getenv("AXME_API_KEY"); + static final String ADDR = SseHelper.env("AXME_AGENT_ADDRESS", "fulfillment-service-agent"); + static final Map SHIPPING = Map.of("express", "1-2 business days", "standard", "3-5 business days", "economy", "7-10 business days"); + + public static void main(String[] args) throws Exception { + if (KEY == null || KEY.isEmpty()) { System.err.println("AXME_API_KEY is required"); System.exit(1); } + AxmeClient client = new AxmeClient(new AxmeClientConfig(BASE, KEY)); + System.out.printf("fulfillment-service-agent starting address=%s%n", ADDR); + int since = 0; + while (true) { + try { + for (Map d : SseHelper.pollAgentStream(BASE, KEY, ADDR, since, 15)) { + String id = SseHelper.str(d, "intent_id"); + Number seq = (Number) d.get("seq"); if (seq != null) since = Math.max(since, seq.intValue()); + if (id.isEmpty()) continue; + Map intent = client.getIntent(id, RequestOptions.none()); + @SuppressWarnings("unchecked") Map p = (Map) intent.getOrDefault("payload", Map.of()); + String orderId = SseHelper.str(p, "order_id"); String shipping = SseHelper.str(p, "shipping", "standard"); + @SuppressWarnings("unchecked") List> items = (List>) p.getOrDefault("items", List.of()); + + Map res = new HashMap<>(); + if (orderId.isEmpty()) { res.put("action", "fail"); res.put("reason", "order_id is required"); } + else if (items.isEmpty()) { res.put("action", "fail"); res.put("reason", "items list is empty"); } + else if (!SHIPPING.containsKey(shipping)) { res.put("action", "fail"); res.put("reason", "unknown shipping"); } + else { + int total = items.stream().mapToInt(i -> i.get("quantity") instanceof Number n ? n.intValue() : 0).sum(); + res.put("action", "complete"); res.put("fulfillment_id", "FUL-" + String.format("%05d", Math.abs((orderId + SseHelper.str(p, "customer_id")).hashCode()) % 100000)); + res.put("tracking_number", "AXME-" + orderId.toUpperCase() + "-" + String.format("%04d", Math.abs(orderId.hashCode()) % 10000)); + res.put("warehouse", "WH-US-EAST-1"); res.put("shipping_method", shipping); res.put("eta", SHIPPING.get(shipping)); + res.put("items_shipped", total); res.put("status", "fulfilled"); + } + client.resumeIntent(id, res, SseHelper.withOwner(ADDR)); + System.out.printf("resumed %s action=%s%n", id, res.get("action")); + } + } catch (Exception e) { Thread.sleep(2000); } + } + } +} diff --git a/examples/delivery/inbox/agent.py b/examples/delivery/inbox/python/agent.py similarity index 100% rename from examples/delivery/inbox/agent.py rename to examples/delivery/inbox/python/agent.py diff --git a/examples/delivery/inbox/check_inbox.py b/examples/delivery/inbox/python/check_inbox.py similarity index 100% rename from examples/delivery/inbox/check_inbox.py rename to examples/delivery/inbox/python/check_inbox.py diff --git a/examples/delivery/inbox/typescript/agent.ts b/examples/delivery/inbox/typescript/agent.ts new file mode 100644 index 0000000..885f987 --- /dev/null +++ b/examples/delivery/inbox/typescript/agent.ts @@ -0,0 +1,73 @@ +/** + * Fulfillment Service Agent — inbox/reply_to delivery binding (TypeScript). + * + * The agent processes orders via stream binding. When completed, AXME + * puts the result into the initiator's inbox (reply_to pattern). + * + * Run: + * export AXME_API_KEY= + * export AXME_AGENT_ADDRESS=fulfillment-service-agent + * npx tsx examples/delivery/inbox/agent.ts + */ +import { AxmeClient } from "@axme/axme"; + +const AXME_BASE_URL = process.env.AXME_BASE_URL ?? "https://api.cloud.axme.ai"; +const AXME_API_KEY = process.env.AXME_API_KEY ?? ""; +const AXME_AGENT_ADDRESS = process.env.AXME_AGENT_ADDRESS ?? "fulfillment-service-agent"; + +if (!AXME_API_KEY) { console.error("AXME_API_KEY is required"); process.exit(1); } + +// --- Business logic --- +const SHIPPING_ESTIMATES: Record = { + express: "1-2 business days", standard: "3-5 business days", economy: "7-10 business days", +}; + +function fulfillOrder(payload: Record): Record { + const orderId = String(payload.order_id ?? ""); + const customer = String(payload.customer_id ?? ""); + const items = (payload.items ?? []) as Array>; + const shipping = String(payload.shipping ?? "standard"); + + if (!orderId) return { action: "fail", reason: "order_id is required" }; + if (!items.length) return { action: "fail", reason: "items list is empty" }; + if (!(shipping in SHIPPING_ESTIMATES)) return { action: "fail", reason: `unknown shipping method='${shipping}'` }; + + const totalItems = items.reduce((sum, i) => sum + (Number(i.quantity) || 0), 0); + if (totalItems <= 0) return { action: "fail", reason: "total item quantity must be > 0" }; + + const h = (s: string) => { let v = 0; for (let i = 0; i < s.length; i++) v = (Math.imul(31, v) + s.charCodeAt(i)) | 0; return Math.abs(v); }; + return { + action: "complete", + fulfillment_id: `FUL-${String(h(orderId + customer) % 100000).padStart(5, "0")}`, + tracking_number: `AXME-${orderId.toUpperCase()}-${String(h(orderId) % 10000).padStart(4, "0")}`, + warehouse: "WH-US-EAST-1", shipping_method: shipping, eta: SHIPPING_ESTIMATES[shipping], + items_shipped: totalItems, status: "fulfilled", + }; +} + +// --- Agent loop --- +const client = new AxmeClient({ apiKey: AXME_API_KEY, baseUrl: AXME_BASE_URL }); +console.log(`fulfillment-service-agent starting address=${AXME_AGENT_ADDRESS} binding=stream`); + +try { + for await (const delivery of client.listen(AXME_AGENT_ADDRESS)) { + const intentId = String(delivery.intent_id ?? ""); + if (!intentId) continue; + console.log(`received order intent ${intentId}`); + + let intent: Record; + try { intent = await client.getIntent(intentId); } catch (e) { console.error(`get_intent(${intentId}) failed:`, e); continue; } + + const payload = (intent.payload ?? {}) as Record; + const result = fulfillOrder(payload); + const action = result.action; + delete result.action; + + try { + await client.resumeIntent(intentId, { action, ...result }, { ownerAgent: AXME_AGENT_ADDRESS }); + console.log(`resumed ${intentId} action=${action}`); + } catch (e) { console.error(`resume_intent(${intentId}) failed:`, e); } + } +} catch (e) { + if ((e as Error).name !== "AbortError") throw e; +} diff --git a/examples/delivery/poll/dotnet/PollAgent.cs b/examples/delivery/poll/dotnet/PollAgent.cs new file mode 100644 index 0000000..48433c4 --- /dev/null +++ b/examples/delivery/poll/dotnet/PollAgent.cs @@ -0,0 +1,51 @@ +using System.Text.Json.Nodes; +using Axme.Sdk; +using AxmeExamples; + +namespace AxmeExamples.Delivery; + +public class PollAgent +{ + public static async Task Main(string[] args) + { + var baseUrl = SseHelper.Env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + var apiKey = SseHelper.Env("AXME_API_KEY"); + var addr = SseHelper.Env("AXME_AGENT_ADDRESS", "batch-processor-agent"); + var interval = int.Parse(SseHelper.Env("POLL_INTERVAL", "10")) * 1000; + if (string.IsNullOrEmpty(apiKey)) { Console.Error.WriteLine("AXME_API_KEY is required"); return; } + + var client = new AxmeClient(new AxmeClientConfig { ApiKey = apiKey, BaseUrl = baseUrl }); + Console.WriteLine($"batch-processor-agent starting address={addr} binding=poll"); + + var counts = new Dictionary { ["transactions"] = 14203, ["invoices"] = 1847, ["orders"] = 3291, ["events"] = 82014 }; + var seen = new HashSet(); + + while (true) + { + try + { + foreach (var d in await SseHelper.PollAgentStreamAsync(baseUrl, apiKey, addr, 0, 2)) + { + var id = SseHelper.Str(d, "intent_id"); + if (string.IsNullOrEmpty(id) || !seen.Add(id)) continue; + var intent = await client.GetIntentAsync(id); + var p = intent["payload"]?.AsObject() ?? new JsonObject(); + var batchId = SseHelper.Str(p, "batch_id", "?"); + var rt = SseHelper.Str(p, "record_type"); + var dr = SseHelper.Str(p, "date_range"); + + JsonObject res; + if (batchId == "?") res = new JsonObject { ["action"] = "fail", ["reason"] = "batch_id is required" }; + else if (!counts.ContainsKey(rt)) res = new JsonObject { ["action"] = "fail", ["reason"] = "unsupported record_type" }; + else if (string.IsNullOrEmpty(dr)) res = new JsonObject { ["action"] = "fail", ["reason"] = "date_range is required" }; + else res = new JsonObject { ["action"] = "complete", ["batch_id"] = batchId, ["record_type"] = rt, ["date_range"] = dr, ["record_count"] = counts[rt], ["status"] = "processed" }; + + await client.ResumeIntentAsync(id, res, new RequestOptions { OwnerAgent = addr }); + Console.WriteLine($"resumed {id} action={res["action"]}"); + } + } + catch { /* timeout normal */ } + await Task.Delay(interval); + } + } +} diff --git a/examples/delivery/poll/go/agent.go b/examples/delivery/poll/go/agent.go new file mode 100644 index 0000000..33457f1 --- /dev/null +++ b/examples/delivery/poll/go/agent.go @@ -0,0 +1,114 @@ +// Batch Processor Agent — poll delivery binding (Go). +// +// Run: +// +// export AXME_API_KEY= +// export AXME_AGENT_ADDRESS=batch-processor-agent +// go run examples/delivery/poll/agent.go +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "strconv" + "strings" + "time" + + "github.com/AxmeAI/axme-sdk-go/axme" +) + +var ( + baseURL = envOr("AXME_BASE_URL", "https://api.cloud.axme.ai") + apiKey = os.Getenv("AXME_API_KEY") + agentAddress = envOr("AXME_AGENT_ADDRESS", "batch-processor-agent") + pollInterval = parseDuration(envOr("POLL_INTERVAL", "10")) +) + +func envOr(k, d string) string { if v := os.Getenv(k); v != "" { return v }; return d } +func str(m map[string]any, k, d string) string { if v, ok := m[k].(string); ok { return v }; return d } + +func parseDuration(s string) time.Duration { + n, _ := strconv.Atoi(s) + if n <= 0 { n = 10 } + return time.Duration(n) * time.Second +} + +var supportedTypes = map[string]int{ + "transactions": 14203, "invoices": 1847, "orders": 3291, "events": 82014, +} + +func processBatch(p map[string]any) map[string]any { + batchID := str(p, "batch_id", "?") + recordType := str(p, "record_type", "") + dateRange := str(p, "date_range", "") + if batchID == "" || batchID == "?" { return map[string]any{"action": "fail", "reason": "batch_id is required"} } + if _, ok := supportedTypes[recordType]; !ok { + return map[string]any{"action": "fail", "reason": fmt.Sprintf("unsupported record_type=%q", recordType)} + } + if dateRange == "" { return map[string]any{"action": "fail", "reason": "date_range is required"} } + return map[string]any{ + "action": "complete", "batch_id": batchID, "record_type": recordType, + "date_range": dateRange, "record_count": supportedTypes[recordType], "status": "processed", + } +} + +func main() { + if apiKey == "" { log.Fatal("AXME_API_KEY is required") } + client, err := axme.NewClient(axme.ClientConfig{APIKey: apiKey, BaseURL: baseURL}) + if err != nil { log.Fatalf("client init: %v", err) } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + log.Printf("batch-processor-agent starting address=%s binding=poll interval=%s", agentAddress, pollInterval) + seen := map[string]bool{} + + for { + select { + case <-ctx.Done(): + log.Println("shutting down") + return + default: + } + + pollCtx, pollCancel := context.WithTimeout(ctx, 5*time.Second) + intents, errs := client.Listen(pollCtx, agentAddress, axme.ListenOptions{WaitSeconds: 2}) + newCount := 0 + drain: + for { + select { + case delivery, ok := <-intents: + if !ok { break drain } + id := str(delivery, "intent_id", "") + if id == "" || seen[id] { continue } + seen[id] = true + log.Printf("processing intent %s", id) + + resp, err := client.GetIntent(ctx, id, axme.RequestOptions{}) + if err != nil { log.Printf("get_intent(%s) failed: %v", id, err); continue } + payload, _ := resp["payload"].(map[string]any) + if payload == nil { payload = map[string]any{} } + + result := processBatch(payload) + action := result["action"] + delete(result, "action") + result["action"] = action + + _, err = client.ResumeIntent(ctx, id, result, axme.RequestOptions{OwnerAgent: agentAddress}) + if err != nil { log.Printf("resume_intent(%s) failed: %v", id, err); continue } + log.Printf("resumed %s action=%s", id, action) + newCount++ + case err, ok := <-errs: + if !ok { break drain } + if !strings.Contains(err.Error(), "context") { log.Printf("poll error: %v", err) } + break drain + } + } + pollCancel() + if newCount > 0 { log.Printf("poll cycle done processed=%d", newCount) } + time.Sleep(pollInterval) + } +} diff --git a/examples/delivery/poll/java/PollAgent.java b/examples/delivery/poll/java/PollAgent.java new file mode 100644 index 0000000..4a58edd --- /dev/null +++ b/examples/delivery/poll/java/PollAgent.java @@ -0,0 +1,51 @@ +package ai.axme.examples.delivery; + +import ai.axme.examples.SseHelper; +import dev.axme.sdk.*; +import java.util.*; + +/** Batch Processor Agent — poll delivery binding (Java). */ +public class PollAgent { + static final String BASE = SseHelper.env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + static final String KEY = System.getenv("AXME_API_KEY"); + static final String ADDR = SseHelper.env("AXME_AGENT_ADDRESS", "batch-processor-agent"); + static final int INTERVAL = Integer.parseInt(SseHelper.env("POLL_INTERVAL", "10")) * 1000; + + static final Map RECORD_COUNTS = Map.of( + "transactions", 14203, "invoices", 1847, "orders", 3291, "events", 82014); + + public static void main(String[] args) throws Exception { + if (KEY == null || KEY.isEmpty()) { System.err.println("AXME_API_KEY is required"); System.exit(1); } + AxmeClient client = new AxmeClient(new AxmeClientConfig(BASE, KEY)); + System.out.printf("batch-processor-agent starting address=%s binding=poll interval=%dms%n", ADDR, INTERVAL); + Set seen = new HashSet<>(); + + while (true) { + try { + for (Map d : SseHelper.pollAgentStream(BASE, KEY, ADDR, 0, 2)) { + String id = SseHelper.str(d, "intent_id"); + if (id.isEmpty() || seen.contains(id)) continue; + seen.add(id); + + Map intent = client.getIntent(id, RequestOptions.none()); + Map p = (intent.get("payload") instanceof Map) ? (Map) intent.get("payload") : Map.of(); + + String batchId = SseHelper.str(p, "batch_id", "?"); + String recordType = SseHelper.str(p, "record_type"); + String dateRange = SseHelper.str(p, "date_range"); + + Map res = new HashMap<>(); + if (batchId.equals("?")) { res.put("action", "fail"); res.put("reason", "batch_id is required"); } + else if (!RECORD_COUNTS.containsKey(recordType)) { res.put("action", "fail"); res.put("reason", "unsupported record_type"); } + else if (dateRange.isEmpty()) { res.put("action", "fail"); res.put("reason", "date_range is required"); } + else { res.put("action", "complete"); res.put("batch_id", batchId); res.put("record_type", recordType); + res.put("date_range", dateRange); res.put("record_count", RECORD_COUNTS.get(recordType)); res.put("status", "processed"); } + + client.resumeIntent(id, res, SseHelper.withOwner(ADDR)); + System.out.printf("resumed %s action=%s%n", id, res.get("action")); + } + } catch (Exception e) { /* timeout normal */ } + Thread.sleep(INTERVAL); + } + } +} diff --git a/examples/delivery/poll/agent.py b/examples/delivery/poll/python/agent.py similarity index 100% rename from examples/delivery/poll/agent.py rename to examples/delivery/poll/python/agent.py diff --git a/examples/delivery/poll/typescript/agent.ts b/examples/delivery/poll/typescript/agent.ts new file mode 100644 index 0000000..0a09089 --- /dev/null +++ b/examples/delivery/poll/typescript/agent.ts @@ -0,0 +1,92 @@ +/** + * Batch Processor Agent — poll delivery binding (TypeScript). + * + * Delivery: poll (periodic reconnect to GET /v1/agents/{address}/intents/stream) + * The agent wakes up every POLL_INTERVAL seconds, checks for new intents, then disconnects. + * + * Run: + * export AXME_API_KEY= + * export AXME_AGENT_ADDRESS=batch-processor-agent + * export POLL_INTERVAL=10 + * npx tsx examples/delivery/poll/agent.ts + */ +import { AxmeClient } from "@axme/axme"; + +const AXME_BASE_URL = process.env.AXME_BASE_URL ?? "https://api.cloud.axme.ai"; +const AXME_API_KEY = process.env.AXME_API_KEY ?? ""; +const AXME_AGENT_ADDRESS = process.env.AXME_AGENT_ADDRESS ?? "batch-processor-agent"; +const POLL_INTERVAL = parseInt(process.env.POLL_INTERVAL ?? "10", 10) * 1000; + +if (!AXME_API_KEY) { console.error("AXME_API_KEY is required"); process.exit(1); } + +// --- Business logic --- +const SUPPORTED_RECORD_TYPES = new Set(["transactions", "invoices", "orders", "events"]); +const RECORD_COUNTS: Record = { + transactions: 14_203, invoices: 1_847, orders: 3_291, events: 82_014, +}; + +function processBatch(payload: Record): Record { + const batchId = String(payload.batch_id ?? "?"); + const recordType = String(payload.record_type ?? ""); + const dateRange = String(payload.date_range ?? ""); + + if (!batchId || batchId === "?") return { action: "fail", reason: "batch_id is required" }; + if (!SUPPORTED_RECORD_TYPES.has(recordType)) { + return { action: "fail", reason: `unsupported record_type='${recordType}', supported: ${[...SUPPORTED_RECORD_TYPES].sort()}` }; + } + if (!dateRange) return { action: "fail", reason: "date_range is required" }; + + return { + action: "complete", batch_id: batchId, record_type: recordType, + date_range: dateRange, record_count: RECORD_COUNTS[recordType] ?? 0, status: "processed", + }; +} + +// --- Poll loop --- +const client = new AxmeClient({ apiKey: AXME_API_KEY, baseUrl: AXME_BASE_URL }); +const seen = new Set(); +let cursor = 0; + +console.log(`batch-processor-agent starting address=${AXME_AGENT_ADDRESS} binding=poll interval=${POLL_INTERVAL / 1000}s`); + +async function pollOnce(): Promise { + let newCount = 0; + try { + for await (const delivery of client.listen(AXME_AGENT_ADDRESS, { since: cursor, waitSeconds: 2, timeoutMs: 5000 })) { + const intentId = String(delivery.intent_id ?? ""); + const seq = typeof delivery.seq === "number" ? delivery.seq : 0; + if (seq) cursor = Math.max(cursor, seq); + if (!intentId || seen.has(intentId)) continue; + seen.add(intentId); + + console.log(`processing intent ${intentId}`); + let intent: Record; + try { + intent = await client.getIntent(intentId); + } catch (e) { console.error(`get_intent(${intentId}) failed:`, e); continue; } + + const payload = (intent.payload ?? {}) as Record; + const result = processBatch(payload); + const action = result.action; + delete result.action; + + try { + await client.resumeIntent(intentId, { action, ...result }, { ownerAgent: AXME_AGENT_ADDRESS }); + console.log(`resumed ${intentId} action=${action}`); + } catch (e) { console.error(`resume_intent(${intentId}) failed:`, e); } + newCount++; + } + } catch { /* timeout — normal */ } + if (newCount) console.log(`poll cycle done processed=${newCount} cursor=${cursor}`); +} + +const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); + +try { + while (true) { + await pollOnce(); + await sleep(POLL_INTERVAL); + } +} catch (e) { + if ((e as Error).name !== "AbortError") throw e; +} diff --git a/examples/delivery/stream/dotnet/StreamAgent.cs b/examples/delivery/stream/dotnet/StreamAgent.cs new file mode 100644 index 0000000..dab47ac --- /dev/null +++ b/examples/delivery/stream/dotnet/StreamAgent.cs @@ -0,0 +1,56 @@ +using System.Text.Json.Nodes; +using Axme.Sdk; +using AxmeExamples; + +namespace AxmeExamples.Delivery; + +public class StreamAgent +{ + public static async Task Main(string[] args) + { + var baseUrl = SseHelper.Env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + var apiKey = SseHelper.Env("AXME_API_KEY"); + var addr = SseHelper.Env("AXME_AGENT_ADDRESS", "compliance-checker-agent"); + if (string.IsNullOrEmpty(apiKey)) { Console.Error.WriteLine("AXME_API_KEY is required"); return; } + + var client = new AxmeClient(new AxmeClientConfig { ApiKey = apiKey, BaseUrl = baseUrl }); + Console.WriteLine($"compliance-checker-agent starting address={addr} binding=stream"); + + string[] blocked = ["production"]; + string[] riskBackup = ["high", "critical"]; + var since = 0; + + while (true) + { + try + { + foreach (var d in await SseHelper.PollAgentStreamAsync(baseUrl, apiKey, addr, since)) + { + var id = SseHelper.Str(d, "intent_id"); + if (d["seq"]?.GetValue() is int seq) since = Math.Max(since, seq); + if (string.IsNullOrEmpty(id)) continue; + + var intent = SseHelper.UnwrapIntent(await client.GetIntentAsync(id)); + if (!SseHelper.IsActionable(intent)) continue; + var p = SseHelper.EffectivePayload(intent); + + var changeId = SseHelper.Str(p, "change_id", "?"); + var env = SseHelper.Str(p, "environment"); + var risk = SseHelper.Str(p, "risk_level", "low"); + var backup = p["backup_confirmed"]?.GetValue() == true; + + bool passed; string reason; + if (blocked.Contains(env)) { passed = false; reason = $"{changeId}: production changes require manual approval"; } + else if (riskBackup.Contains(risk) && !backup) { passed = false; reason = $"{changeId}: risk_level={risk} requires backup_confirmed=true"; } + else { passed = true; reason = $"{changeId}: compliance checks passed"; } + + var res = new JsonObject { ["action"] = passed ? "complete" : "fail", ["passed"] = passed, ["reason"] = reason }; + if (passed) res["checked_fields"] = new JsonArray("environment", "risk_level", "backup_confirmed"); + await client.ResumeIntentAsync(id, res, new RequestOptions { OwnerAgent = addr }); + Console.WriteLine($"resumed {id} (passed={passed})"); + } + } + catch (Exception ex) { Console.Error.WriteLine($"stream error: {ex.Message}"); await Task.Delay(2000); } + } + } +} diff --git a/examples/delivery/stream/go/agent.go b/examples/delivery/stream/go/agent.go new file mode 100644 index 0000000..721e9b8 --- /dev/null +++ b/examples/delivery/stream/go/agent.go @@ -0,0 +1,148 @@ +// Compliance Checker Agent — stream delivery binding (Go). +// +// Run: +// +// export AXME_API_KEY= +// export AXME_AGENT_ADDRESS=compliance-checker-agent +// go run examples/delivery/stream/agent.go +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "strings" + + "github.com/AxmeAI/axme-sdk-go/axme" +) + +var ( + baseURL = envOr("AXME_BASE_URL", "https://api.cloud.axme.ai") + apiKey = os.Getenv("AXME_API_KEY") + agentAddress = envOr("AXME_AGENT_ADDRESS", "compliance-checker-agent") +) + +func envOr(key, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + return def +} + +// --- Business logic --- + +var ( + blockedEnvironments = map[string]bool{"production": true} + riskRequiresBackup = map[string]bool{"high": true, "critical": true} +) + +func checkCompliance(payload map[string]any) (bool, string) { + changeID := str(payload, "change_id", "?") + environment := str(payload, "environment", "") + riskLevel := str(payload, "risk_level", "low") + backupOk, _ := payload["backup_confirmed"].(bool) + + if blockedEnvironments[environment] { + return false, fmt.Sprintf("%s: production changes require manual approval, not automated", changeID) + } + if riskRequiresBackup[riskLevel] && !backupOk { + return false, fmt.Sprintf("%s: risk_level=%s requires backup_confirmed=true", changeID, riskLevel) + } + return true, fmt.Sprintf("%s: compliance checks passed (environment=%s, risk=%s)", changeID, environment, riskLevel) +} + +func str(m map[string]any, key, def string) string { + if v, ok := m[key].(string); ok && v != "" { + return v + } + return def +} + +func main() { + if apiKey == "" { + log.Fatal("AXME_API_KEY is required") + } + + client, err := axme.NewClient(axme.ClientConfig{APIKey: apiKey, BaseURL: baseURL}) + if err != nil { + log.Fatalf("client init failed: %v", err) + } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + log.Printf("compliance-checker-agent starting address=%s binding=stream", agentAddress) + + intents, errs := client.Listen(ctx, agentAddress, axme.ListenOptions{}) + for { + select { + case delivery, ok := <-intents: + if !ok { + return + } + intentID := str(delivery, "intent_id", "") + if intentID == "" { + continue + } + log.Printf("received intent %s", intentID) + handleIntent(ctx, client, intentID) + case err, ok := <-errs: + if !ok { + return + } + log.Printf("stream error: %v", err) + case <-ctx.Done(): + log.Println("shutting down") + return + } + } +} + +func handleIntent(ctx context.Context, client *axme.Client, intentID string) { + resp, err := client.GetIntent(ctx, intentID, axme.RequestOptions{}) + if err != nil { + log.Printf("get_intent(%s) failed: %v", intentID, err) + return + } + intent, _ := resp["intent"].(map[string]any) + if intent == nil { + intent = resp + } + + status := strings.ToUpper(str(intent, "lifecycle_status", str(intent, "status", ""))) + actionable := map[string]bool{"CREATED": true, "DELIVERED": true, "ACKNOWLEDGED": true, "IN_PROGRESS": true, "WAITING": true} + if !actionable[status] { + return + } + + rawPayload, _ := intent["payload"].(map[string]any) + if rawPayload == nil { + rawPayload = map[string]any{} + } + effectivePayload, _ := rawPayload["parent_payload"].(map[string]any) + if effectivePayload == nil { + effectivePayload = rawPayload + } + + passed, reason := checkCompliance(effectivePayload) + log.Printf("compliance result for %s: passed=%v reason=%s", intentID, passed, reason) + + var resumePayload map[string]any + if passed { + resumePayload = map[string]any{ + "action": "complete", "passed": true, "reason": reason, + "checked_fields": []string{"environment", "risk_level", "backup_confirmed"}, + } + } else { + resumePayload = map[string]any{"action": "fail", "passed": false, "reason": reason} + } + + _, err = client.ResumeIntent(ctx, intentID, resumePayload, axme.RequestOptions{OwnerAgent: agentAddress}) + if err != nil { + log.Printf("resume_intent(%s) failed: %v", intentID, err) + return + } + log.Printf("resumed intent %s (passed=%v)", intentID, passed) +} diff --git a/examples/delivery/stream/java/StreamAgent.java b/examples/delivery/stream/java/StreamAgent.java new file mode 100644 index 0000000..a089f5a --- /dev/null +++ b/examples/delivery/stream/java/StreamAgent.java @@ -0,0 +1,53 @@ +package ai.axme.examples.delivery; + +import ai.axme.examples.SseHelper; +import dev.axme.sdk.*; +import java.util.*; + +/** Compliance Checker Agent — stream delivery binding (Java). */ +public class StreamAgent { + static final String BASE = SseHelper.env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + static final String KEY = System.getenv("AXME_API_KEY"); + static final String ADDR = SseHelper.env("AXME_AGENT_ADDRESS", "compliance-checker-agent"); + + static final Set BLOCKED = Set.of("production"); + static final Set RISK_BACKUP = Set.of("high", "critical"); + + public static void main(String[] args) throws Exception { + if (KEY == null || KEY.isEmpty()) { System.err.println("AXME_API_KEY is required"); System.exit(1); } + AxmeClient client = new AxmeClient(new AxmeClientConfig(BASE, KEY)); + System.out.printf("compliance-checker-agent starting address=%s binding=stream%n", ADDR); + + int since = 0; + while (true) { + try { + for (Map d : SseHelper.pollAgentStream(BASE, KEY, ADDR, since, 15)) { + String id = SseHelper.str(d, "intent_id"); + Number seq = (Number) d.get("seq"); + if (seq != null) since = Math.max(since, seq.intValue()); + if (id.isEmpty()) continue; + + Map intent = SseHelper.unwrapIntent(client.getIntent(id, RequestOptions.none())); + if (!SseHelper.isActionable(intent)) continue; + Map p = SseHelper.effectivePayload(intent); + + String changeId = SseHelper.str(p, "change_id", "?"); + String env = SseHelper.str(p, "environment"); + String risk = SseHelper.str(p, "risk_level", "low"); + boolean backup = Boolean.TRUE.equals(p.get("backup_confirmed")); + + boolean passed; String reason; + if (BLOCKED.contains(env)) { passed = false; reason = changeId + ": production changes require manual approval"; } + else if (RISK_BACKUP.contains(risk) && !backup) { passed = false; reason = changeId + ": risk_level=" + risk + " requires backup_confirmed=true"; } + else { passed = true; reason = changeId + ": compliance checks passed"; } + + Map res = new HashMap<>(); + res.put("action", passed ? "complete" : "fail"); res.put("passed", passed); res.put("reason", reason); + if (passed) res.put("checked_fields", List.of("environment", "risk_level", "backup_confirmed")); + client.resumeIntent(id, res, SseHelper.withOwner(ADDR)); + System.out.printf("resumed %s (passed=%s)%n", id, passed); + } + } catch (Exception e) { System.err.println("stream error: " + e.getMessage()); Thread.sleep(2000); } + } + } +} diff --git a/examples/delivery/stream/agent.py b/examples/delivery/stream/python/agent.py similarity index 100% rename from examples/delivery/stream/agent.py rename to examples/delivery/stream/python/agent.py diff --git a/examples/delivery/stream/typescript/agent.ts b/examples/delivery/stream/typescript/agent.ts new file mode 100644 index 0000000..91de99a --- /dev/null +++ b/examples/delivery/stream/typescript/agent.ts @@ -0,0 +1,116 @@ +/** + * Compliance Checker Agent — stream delivery binding (TypeScript). + * + * Delivery: stream (GET /v1/agents/{address}/intents/stream) + * The agent holds a persistent SSE connection to AXME. + * + * Run: + * export AXME_API_KEY= + * export AXME_AGENT_ADDRESS=compliance-checker-agent + * npx tsx examples/delivery/stream/agent.ts + */ +import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { AxmeClient } from "@axme/axme"; + +const AXME_BASE_URL = process.env.AXME_BASE_URL ?? "https://api.cloud.axme.ai"; +const AXME_API_KEY = process.env.AXME_API_KEY ?? ""; +const AXME_AGENT_ADDRESS = process.env.AXME_AGENT_ADDRESS ?? "compliance-checker-agent"; + +if (!AXME_API_KEY) { + console.error("AXME_API_KEY is required"); + process.exit(1); +} + +// --- Delivery cursor --- +const SEQ_FILE = join(homedir(), ".axme", "compliance_checker_agent_seq.txt"); + +function loadSince(): number { + try { + return parseInt(readFileSync(SEQ_FILE, "utf-8").trim(), 10) || 0; + } catch { + return 0; + } +} + +function saveSince(seq: number): void { + try { + mkdirSync(join(homedir(), ".axme"), { recursive: true }); + writeFileSync(SEQ_FILE, String(seq), "utf-8"); + } catch (e) { + console.warn("could not persist delivery cursor:", e); + } +} + +// --- Business logic --- +const BLOCKED_ENVIRONMENTS = new Set(["production"]); +const RISK_REQUIRES_BACKUP = new Set(["high", "critical"]); + +function checkCompliance(payload: Record): { passed: boolean; reason: string } { + const changeId = String(payload.change_id ?? "?"); + const environment = String(payload.environment ?? ""); + const riskLevel = String(payload.risk_level ?? "low"); + const backupOk = Boolean(payload.backup_confirmed); + + if (BLOCKED_ENVIRONMENTS.has(environment)) { + return { passed: false, reason: `${changeId}: production changes require manual approval, not automated` }; + } + if (RISK_REQUIRES_BACKUP.has(riskLevel) && !backupOk) { + return { passed: false, reason: `${changeId}: risk_level=${riskLevel} requires backup_confirmed=true` }; + } + return { passed: true, reason: `${changeId}: compliance checks passed (environment=${environment}, risk=${riskLevel})` }; +} + +// --- Agent loop --- +const client = new AxmeClient({ apiKey: AXME_API_KEY, baseUrl: AXME_BASE_URL }); +const since = loadSince(); + +console.log(`compliance-checker-agent starting address=${AXME_AGENT_ADDRESS} binding=stream since=${since}`); + +try { + for await (const delivery of client.listen(AXME_AGENT_ADDRESS, { since })) { + const intentId = String(delivery.intent_id ?? ""); + const seq = typeof delivery.seq === "number" ? delivery.seq : 0; + if (!intentId) continue; + + console.log(`received intent ${intentId}`); + + let intent: Record; + try { + const resp = await client.getIntent(intentId); + intent = (resp.intent ?? resp) as Record; + } catch (e) { + console.error(`get_intent(${intentId}) failed:`, e); + continue; + } + + const status = String(intent.lifecycle_status ?? intent.status ?? "").toUpperCase(); + if (!["CREATED", "DELIVERED", "ACKNOWLEDGED", "IN_PROGRESS", "WAITING"].includes(status)) continue; + + const rawPayload = (intent.payload ?? {}) as Record; + const effectivePayload = (rawPayload.parent_payload ?? rawPayload) as Record; + const { passed, reason } = checkCompliance(effectivePayload); + + console.log(`compliance result for ${intentId}: passed=${passed} reason=${reason}`); + + try { + if (passed) { + await client.resumeIntent(intentId, { + action: "complete", passed: true, reason, + checked_fields: ["environment", "risk_level", "backup_confirmed"], + }, { ownerAgent: AXME_AGENT_ADDRESS }); + } else { + await client.resumeIntent(intentId, { + action: "fail", passed: false, reason, + }, { ownerAgent: AXME_AGENT_ADDRESS }); + } + console.log(`resumed intent ${intentId} (passed=${passed})`); + if (seq > 0) saveSince(seq); + } catch (e) { + console.error(`resume_intent(${intentId}) failed:`, e); + } + } +} catch (e) { + if ((e as Error).name !== "AbortError") throw e; +} diff --git a/examples/durability/reminder-escalation/dotnet/ReminderEscalationAgent.cs b/examples/durability/reminder-escalation/dotnet/ReminderEscalationAgent.cs new file mode 100644 index 0000000..ecb3401 --- /dev/null +++ b/examples/durability/reminder-escalation/dotnet/ReminderEscalationAgent.cs @@ -0,0 +1,51 @@ +using System.Text.Json.Nodes; +using Axme.Sdk; +using AxmeExamples; + +namespace AxmeExamples.Durability; + +public class ReminderEscalationAgent +{ + public static async Task Main(string[] args) + { + var baseUrl = SseHelper.Env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + var apiKey = SseHelper.Env("AXME_API_KEY"); + var addr = SseHelper.Env("AXME_AGENT_ADDRESS", "pre-approval-validator"); + if (string.IsNullOrEmpty(apiKey)) { Console.Error.WriteLine("AXME_API_KEY is required"); return; } + + var client = new AxmeClient(new AxmeClientConfig { ApiKey = apiKey, BaseUrl = baseUrl }); + Console.WriteLine($"pre-approval-validator starting address={addr}"); + string[] types = ["config_update", "dependency_update", "rollback", "hotfix"]; + string[] risks = ["low", "medium", "high", "critical"]; + var since = 0; + + while (true) + { + try + { + foreach (var d in await SseHelper.PollAgentStreamAsync(baseUrl, apiKey, addr, since)) + { + var id = SseHelper.Str(d, "intent_id"); + if (d["seq"]?.GetValue() is int seq) since = Math.Max(since, seq); + if (string.IsNullOrEmpty(id)) continue; + var intent = SseHelper.UnwrapIntent(await client.GetIntentAsync(id)); + if (!SseHelper.IsActionable(intent)) continue; + var p = SseHelper.EffectivePayload(intent); + + var cid = SseHelper.Str(p, "change_id", "?"); var ct = SseHelper.Str(p, "change_type"); + var rl = SseHelper.Str(p, "risk_level"); var svc = SseHelper.Str(p, "service"); + bool valid; string reason; + if (string.IsNullOrEmpty(svc)) { valid = false; reason = $"{cid}: service required"; } + else if (!types.Contains(ct)) { valid = false; reason = $"{cid}: change_type not allowed"; } + else if (!risks.Contains(rl)) { valid = false; reason = $"{cid}: risk_level not valid"; } + else { valid = true; reason = $"{cid}: pre-validation passed for {svc}"; } + + var res = new JsonObject { ["action"] = valid ? "complete" : "fail", ["valid"] = valid, ["reason"] = reason }; + await client.ResumeIntentAsync(id, res, new RequestOptions { OwnerAgent = addr }); + Console.WriteLine($"resumed {id} (valid={valid})"); + } + } + catch { await Task.Delay(2000); } + } + } +} diff --git a/examples/durability/reminder-escalation/go/agent.go b/examples/durability/reminder-escalation/go/agent.go new file mode 100644 index 0000000..5a2d36f --- /dev/null +++ b/examples/durability/reminder-escalation/go/agent.go @@ -0,0 +1,84 @@ +// Pre-Approval Validator — durability/reminder-escalation example (Go). +// +// Run: +// +// export AXME_API_KEY= +// export AXME_AGENT_ADDRESS=pre-approval-validator +// go run examples/durability/reminder-escalation/agent.go +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "sort" + "strings" + + "github.com/AxmeAI/axme-sdk-go/axme" +) + +var ( + baseURL = envOr("AXME_BASE_URL", "https://api.cloud.axme.ai") + apiKey = os.Getenv("AXME_API_KEY") + agentAddress = envOr("AXME_AGENT_ADDRESS", "pre-approval-validator") +) + +func envOr(k, d string) string { if v := os.Getenv(k); v != "" { return v }; return d } +func str(m map[string]any, k, d string) string { if v, ok := m[k].(string); ok { return v }; return d } + +var allowedTypes = map[string]bool{"config_update": true, "dependency_update": true, "rollback": true, "hotfix": true} +var validRisks = map[string]bool{"low": true, "medium": true, "high": true, "critical": true} + +func sortedKeys(m map[string]bool) []string { + keys := make([]string, 0, len(m)) + for k := range m { keys = append(keys, k) } + sort.Strings(keys) + return keys +} + +func validateChange(p map[string]any) (bool, string) { + changeID := str(p, "change_id", "?") + changeType := str(p, "change_type", "") + riskLevel := str(p, "risk_level", "") + service := str(p, "service", "") + + if service == "" { return false, fmt.Sprintf("%s: service is required", changeID) } + if !allowedTypes[changeType] { return false, fmt.Sprintf("%s: change_type '%s' not in %v", changeID, changeType, sortedKeys(allowedTypes)) } + if !validRisks[riskLevel] { return false, fmt.Sprintf("%s: risk_level '%s' not in %v", changeID, riskLevel, sortedKeys(validRisks)) } + return true, fmt.Sprintf("%s: pre-validation passed for '%s' (%s, %s risk)", changeID, service, changeType, riskLevel) +} + +func main() { + if apiKey == "" { log.Fatal("AXME_API_KEY is required") } + client, err := axme.NewClient(axme.ClientConfig{APIKey: apiKey, BaseURL: baseURL}) + if err != nil { log.Fatalf("client init: %v", err) } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + log.Printf("pre-approval-validator starting address=%s binding=stream", agentAddress) + + intents, errs := client.Listen(ctx, agentAddress, axme.ListenOptions{}) + for { + select { + case d, ok := <-intents: + if !ok { return } + id := str(d, "intent_id", ""); if id == "" { continue } + resp, _ := client.GetIntent(ctx, id, axme.RequestOptions{}) + intent, _ := resp["intent"].(map[string]any); if intent == nil { intent = resp } + status := strings.ToUpper(str(intent, "lifecycle_status", str(intent, "status", ""))) + if !map[string]bool{"CREATED": true, "DELIVERED": true, "ACKNOWLEDGED": true, "IN_PROGRESS": true, "WAITING": true}[status] { continue } + raw, _ := intent["payload"].(map[string]any); if raw == nil { raw = map[string]any{} } + eff, _ := raw["parent_payload"].(map[string]any); if eff == nil { eff = raw } + + valid, reason := validateChange(eff) + log.Printf("pre-validation for %s: valid=%v", id, valid) + action := "complete"; if !valid { action = "fail" } + client.ResumeIntent(ctx, id, map[string]any{"action": action, "valid": valid, "reason": reason}, axme.RequestOptions{OwnerAgent: agentAddress}) + log.Printf("resumed %s (valid=%v)", id, valid) + case err, ok := <-errs: if !ok { return }; log.Printf("error: %v", err) + case <-ctx.Done(): return + } + } +} diff --git a/examples/durability/reminder-escalation/java/ReminderEscalationAgent.java b/examples/durability/reminder-escalation/java/ReminderEscalationAgent.java new file mode 100644 index 0000000..b2902b2 --- /dev/null +++ b/examples/durability/reminder-escalation/java/ReminderEscalationAgent.java @@ -0,0 +1,44 @@ +package ai.axme.examples.durability; + +import ai.axme.examples.SseHelper; +import dev.axme.sdk.*; +import java.util.*; + +/** Pre-Approval Validator — durability/reminder-escalation (Java). */ +public class ReminderEscalationAgent { + static final String BASE = SseHelper.env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + static final String KEY = System.getenv("AXME_API_KEY"); + static final String ADDR = SseHelper.env("AXME_AGENT_ADDRESS", "pre-approval-validator"); + static final Set TYPES = Set.of("config_update", "dependency_update", "rollback", "hotfix"); + static final Set RISKS = Set.of("low", "medium", "high", "critical"); + + public static void main(String[] args) throws Exception { + if (KEY == null || KEY.isEmpty()) { System.err.println("AXME_API_KEY is required"); System.exit(1); } + AxmeClient client = new AxmeClient(new AxmeClientConfig(BASE, KEY)); + System.out.printf("pre-approval-validator starting address=%s%n", ADDR); + int since = 0; + while (true) { + try { + for (Map d : SseHelper.pollAgentStream(BASE, KEY, ADDR, since, 15)) { + String id = SseHelper.str(d, "intent_id"); Number seq = (Number) d.get("seq"); + if (seq != null) since = Math.max(since, seq.intValue()); if (id.isEmpty()) continue; + Map intent = SseHelper.unwrapIntent(client.getIntent(id, RequestOptions.none())); + if (!SseHelper.isActionable(intent)) continue; + Map p = SseHelper.effectivePayload(intent); + + String cid = SseHelper.str(p, "change_id", "?"), ct = SseHelper.str(p, "change_type"); + String rl = SseHelper.str(p, "risk_level"), svc = SseHelper.str(p, "service"); + boolean valid = true; String reason; + if (svc.isEmpty()) { valid = false; reason = cid + ": service is required"; } + else if (!TYPES.contains(ct)) { valid = false; reason = cid + ": change_type not allowed"; } + else if (!RISKS.contains(rl)) { valid = false; reason = cid + ": risk_level not valid"; } + else { reason = cid + ": pre-validation passed for " + svc; } + + client.resumeIntent(id, Map.of("action", valid ? "complete" : "fail", "valid", valid, "reason", reason), + SseHelper.withOwner(ADDR)); + System.out.printf("resumed %s (valid=%s)%n", id, valid); + } + } catch (Exception e) { Thread.sleep(2000); } + } + } +} diff --git a/examples/durability/reminder-escalation/agent.py b/examples/durability/reminder-escalation/python/agent.py similarity index 100% rename from examples/durability/reminder-escalation/agent.py rename to examples/durability/reminder-escalation/python/agent.py diff --git a/examples/durability/reminder-escalation/typescript/agent.ts b/examples/durability/reminder-escalation/typescript/agent.ts new file mode 100644 index 0000000..c970a93 --- /dev/null +++ b/examples/durability/reminder-escalation/typescript/agent.ts @@ -0,0 +1,70 @@ +/** + * Pre-Approval Validator — durability/reminder-escalation example (TypeScript). + * + * Validates a change request before routing to human approval with SLA reminders. + * + * Run: + * export AXME_API_KEY= + * export AXME_AGENT_ADDRESS=pre-approval-validator + * npx tsx examples/durability/reminder-escalation/agent.ts + */ +import { AxmeClient } from "@axme/axme"; + +const AXME_BASE_URL = process.env.AXME_BASE_URL ?? "https://api.cloud.axme.ai"; +const AXME_API_KEY = process.env.AXME_API_KEY ?? ""; +const AXME_AGENT_ADDRESS = process.env.AXME_AGENT_ADDRESS ?? "pre-approval-validator"; + +if (!AXME_API_KEY) { console.error("AXME_API_KEY is required"); process.exit(1); } + +const ALLOWED_CHANGE_TYPES = new Set(["config_update", "dependency_update", "rollback", "hotfix"]); +const VALID_RISK_LEVELS = new Set(["low", "medium", "high", "critical"]); + +function validateChange(payload: Record): { valid: boolean; reason: string } { + const changeId = String(payload.change_id ?? "?"); + const changeType = String(payload.change_type ?? ""); + const riskLevel = String(payload.risk_level ?? ""); + const service = String(payload.service ?? ""); + + if (!service) return { valid: false, reason: `${changeId}: service is required` }; + if (!ALLOWED_CHANGE_TYPES.has(changeType)) return { valid: false, reason: `${changeId}: change_type '${changeType}' not in ${[...ALLOWED_CHANGE_TYPES].sort()}` }; + if (!VALID_RISK_LEVELS.has(riskLevel)) return { valid: false, reason: `${changeId}: risk_level '${riskLevel}' not in ${[...VALID_RISK_LEVELS].sort()}` }; + + return { valid: true, reason: `${changeId}: pre-validation passed for '${service}' (${changeType}, ${riskLevel} risk)` }; +} + +const client = new AxmeClient({ apiKey: AXME_API_KEY, baseUrl: AXME_BASE_URL }); +console.log(`pre-approval-validator starting address=${AXME_AGENT_ADDRESS} binding=stream`); + +try { + for await (const delivery of client.listen(AXME_AGENT_ADDRESS)) { + const intentId = String(delivery.intent_id ?? ""); + if (!intentId) continue; + console.log(`received intent ${intentId}`); + + let intent: Record; + try { + const resp = await client.getIntent(intentId); + intent = (resp.intent ?? resp) as Record; + } catch (e) { console.error(`get_intent(${intentId}) failed:`, e); continue; } + + const status = String(intent.lifecycle_status ?? intent.status ?? "").toUpperCase(); + if (!["CREATED", "DELIVERED", "ACKNOWLEDGED", "IN_PROGRESS", "WAITING"].includes(status)) continue; + + const rawPayload = (intent.payload ?? {}) as Record; + const effectivePayload = (rawPayload.parent_payload ?? rawPayload) as Record; + const { valid, reason } = validateChange(effectivePayload); + + console.log(`pre-validation for ${intentId}: valid=${valid} reason=${reason}`); + + try { + if (valid) { + await client.resumeIntent(intentId, { action: "complete", valid: true, reason }, { ownerAgent: AXME_AGENT_ADDRESS }); + } else { + await client.resumeIntent(intentId, { action: "fail", valid: false, reason }, { ownerAgent: AXME_AGENT_ADDRESS }); + } + console.log(`resumed intent ${intentId} (valid=${valid})`); + } catch (e) { console.error(`resume_intent(${intentId}) failed:`, e); } + } +} catch (e) { + if ((e as Error).name !== "AbortError") throw e; +} diff --git a/examples/durability/retry-failure/dotnet/RetryFailureAgent.cs b/examples/durability/retry-failure/dotnet/RetryFailureAgent.cs new file mode 100644 index 0000000..88753a4 --- /dev/null +++ b/examples/durability/retry-failure/dotnet/RetryFailureAgent.cs @@ -0,0 +1,48 @@ +using System.Security.Cryptography; +using System.Text; +using System.Text.Json.Nodes; +using Axme.Sdk; +using AxmeExamples; + +namespace AxmeExamples.Durability; + +public class RetryFailureAgent +{ + public static async Task Main(string[] args) + { + var baseUrl = SseHelper.Env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + var apiKey = SseHelper.Env("AXME_API_KEY"); + var addr = SseHelper.Env("AXME_AGENT_ADDRESS", "webhook-receiver-agent"); + if (string.IsNullOrEmpty(apiKey)) { Console.Error.WriteLine("AXME_API_KEY is required"); return; } + + var client = new AxmeClient(new AxmeClientConfig { ApiKey = apiKey, BaseUrl = baseUrl }); + Console.WriteLine($"webhook-receiver starting address={addr}"); + var since = 0; + + while (true) + { + try + { + foreach (var d in await SseHelper.PollAgentStreamAsync(baseUrl, apiKey, addr, since)) + { + var id = SseHelper.Str(d, "intent_id"); + if (d["seq"]?.GetValue() is int seq) since = Math.Max(since, seq); + if (string.IsNullOrEmpty(id)) continue; + var intent = SseHelper.UnwrapIntent(await client.GetIntentAsync(id)); + if (!SseHelper.IsActionable(intent)) continue; + var p = SseHelper.EffectivePayload(intent); + + var et = SseHelper.Str(p, "event_type", "unknown"); var src = SseHelper.Str(p, "source"); + var tid = SseHelper.Str(p, "test_id"); + var fp = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes($"{et}:{src}:{tid}"))).ToLower()[..16]; + + var res = new JsonObject { ["action"] = "complete", ["event_type"] = et, ["source"] = src, + ["fingerprint"] = fp, ["processed"] = true, ["note"] = $"webhook '{et}' processed" }; + await client.ResumeIntentAsync(id, res, new RequestOptions { OwnerAgent = addr }); + Console.WriteLine($"resumed {id} fingerprint={fp}"); + } + } + catch { await Task.Delay(2000); } + } + } +} diff --git a/examples/durability/retry-failure/go/agent.go b/examples/durability/retry-failure/go/agent.go new file mode 100644 index 0000000..b72e663 --- /dev/null +++ b/examples/durability/retry-failure/go/agent.go @@ -0,0 +1,71 @@ +// Webhook Receiver Agent — durability/retry-failure example (Go). +// +// Run: +// +// export AXME_API_KEY= +// export AXME_AGENT_ADDRESS=webhook-receiver-agent +// go run examples/durability/retry-failure/agent.go +package main + +import ( + "context" + "crypto/sha256" + "fmt" + "log" + "os" + "os/signal" + "strings" + + "github.com/AxmeAI/axme-sdk-go/axme" +) + +var ( + baseURL = envOr("AXME_BASE_URL", "https://api.cloud.axme.ai") + apiKey = os.Getenv("AXME_API_KEY") + agentAddress = envOr("AXME_AGENT_ADDRESS", "webhook-receiver-agent") +) + +func envOr(k, d string) string { if v := os.Getenv(k); v != "" { return v }; return d } +func str(m map[string]any, k, d string) string { if v, ok := m[k].(string); ok { return v }; return d } + +func processWebhook(p map[string]any) map[string]any { + eventType := str(p, "event_type", "unknown") + source := str(p, "source", "") + testID := str(p, "test_id", "") + fp := fmt.Sprintf("%x", sha256.Sum256([]byte(eventType+":"+source+":"+testID)))[:16] + return map[string]any{"event_type": eventType, "source": source, "fingerprint": fp, "processed": true, + "note": fmt.Sprintf("webhook event '%s' processed from '%s'", eventType, source)} +} + +func main() { + if apiKey == "" { log.Fatal("AXME_API_KEY is required") } + client, err := axme.NewClient(axme.ClientConfig{APIKey: apiKey, BaseURL: baseURL}) + if err != nil { log.Fatalf("client init: %v", err) } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + log.Printf("webhook-receiver starting address=%s binding=stream", agentAddress) + + intents, errs := client.Listen(ctx, agentAddress, axme.ListenOptions{}) + for { + select { + case d, ok := <-intents: + if !ok { return } + id := str(d, "intent_id", ""); if id == "" { continue } + resp, _ := client.GetIntent(ctx, id, axme.RequestOptions{}) + intent, _ := resp["intent"].(map[string]any); if intent == nil { intent = resp } + status := strings.ToUpper(str(intent, "lifecycle_status", str(intent, "status", ""))) + if !map[string]bool{"CREATED": true, "DELIVERED": true, "ACKNOWLEDGED": true, "IN_PROGRESS": true, "WAITING": true}[status] { continue } + raw, _ := intent["payload"].(map[string]any); if raw == nil { raw = map[string]any{} } + eff, _ := raw["parent_payload"].(map[string]any); if eff == nil { eff = raw } + + result := processWebhook(eff) + result["action"] = "complete" + log.Printf("processed webhook for %s: fingerprint=%s", id, result["fingerprint"]) + client.ResumeIntent(ctx, id, result, axme.RequestOptions{OwnerAgent: agentAddress}) + log.Printf("resumed %s", id) + case err, ok := <-errs: if !ok { return }; log.Printf("error: %v", err) + case <-ctx.Done(): return + } + } +} diff --git a/examples/durability/retry-failure/java/RetryFailureAgent.java b/examples/durability/retry-failure/java/RetryFailureAgent.java new file mode 100644 index 0000000..0a659bb --- /dev/null +++ b/examples/durability/retry-failure/java/RetryFailureAgent.java @@ -0,0 +1,46 @@ +package ai.axme.examples.durability; + +import ai.axme.examples.SseHelper; +import dev.axme.sdk.*; +import java.security.MessageDigest; +import java.util.*; + +/** Webhook Receiver Agent — durability/retry-failure (Java). */ +public class RetryFailureAgent { + static final String BASE = SseHelper.env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + static final String KEY = System.getenv("AXME_API_KEY"); + static final String ADDR = SseHelper.env("AXME_AGENT_ADDRESS", "webhook-receiver-agent"); + + public static void main(String[] args) throws Exception { + if (KEY == null || KEY.isEmpty()) { System.err.println("AXME_API_KEY is required"); System.exit(1); } + AxmeClient client = new AxmeClient(new AxmeClientConfig(BASE, KEY)); + System.out.printf("webhook-receiver starting address=%s%n", ADDR); + int since = 0; + while (true) { + try { + for (Map d : SseHelper.pollAgentStream(BASE, KEY, ADDR, since, 15)) { + String id = SseHelper.str(d, "intent_id"); Number seq = (Number) d.get("seq"); + if (seq != null) since = Math.max(since, seq.intValue()); if (id.isEmpty()) continue; + Map intent = SseHelper.unwrapIntent(client.getIntent(id, RequestOptions.none())); + if (!SseHelper.isActionable(intent)) continue; + Map p = SseHelper.effectivePayload(intent); + + String et = SseHelper.str(p, "event_type", "unknown"), src = SseHelper.str(p, "source"); + String tid = SseHelper.str(p, "test_id"); + MessageDigest md = MessageDigest.getInstance("SHA-256"); + String fp = bytesToHex(md.digest((et + ":" + src + ":" + tid).getBytes())).substring(0, 16); + + client.resumeIntent(id, Map.of("action", "complete", "event_type", et, "source", src, + "fingerprint", fp, "processed", true, "note", "webhook processed"), SseHelper.withOwner(ADDR)); + System.out.printf("resumed %s fingerprint=%s%n", id, fp); + } + } catch (Exception e) { Thread.sleep(2000); } + } + } + + static String bytesToHex(byte[] b) { + StringBuilder sb = new StringBuilder(); + for (byte v : b) sb.append(String.format("%02x", v)); + return sb.toString(); + } +} diff --git a/examples/durability/retry-failure/agent.py b/examples/durability/retry-failure/python/agent.py similarity index 100% rename from examples/durability/retry-failure/agent.py rename to examples/durability/retry-failure/python/agent.py diff --git a/examples/durability/retry-failure/typescript/agent.ts b/examples/durability/retry-failure/typescript/agent.ts new file mode 100644 index 0000000..a42159e --- /dev/null +++ b/examples/durability/retry-failure/typescript/agent.ts @@ -0,0 +1,60 @@ +/** + * Webhook Receiver Agent — durability/retry-failure example (TypeScript). + * + * Processes webhook events idempotently. Run WITHOUT the agent to observe + * AXME's retry-then-fail behavior. + * + * Run: + * export AXME_API_KEY= + * export AXME_AGENT_ADDRESS=webhook-receiver-agent + * npx tsx examples/durability/retry-failure/agent.ts + */ +import { createHash } from "node:crypto"; +import { AxmeClient } from "@axme/axme"; + +const AXME_BASE_URL = process.env.AXME_BASE_URL ?? "https://api.cloud.axme.ai"; +const AXME_API_KEY = process.env.AXME_API_KEY ?? ""; +const AXME_AGENT_ADDRESS = process.env.AXME_AGENT_ADDRESS ?? "webhook-receiver-agent"; + +if (!AXME_API_KEY) { console.error("AXME_API_KEY is required"); process.exit(1); } + +function processWebhook(payload: Record): Record { + const eventType = String(payload.event_type ?? "unknown"); + const source = String(payload.source ?? ""); + const testId = String(payload.test_id ?? ""); + const fingerprint = createHash("sha256").update(`${eventType}:${source}:${testId}`).digest("hex").slice(0, 16); + return { event_type: eventType, source, fingerprint, processed: true, note: `webhook event '${eventType}' processed from '${source}'` }; +} + +const client = new AxmeClient({ apiKey: AXME_API_KEY, baseUrl: AXME_BASE_URL }); +console.log(`webhook-receiver starting address=${AXME_AGENT_ADDRESS} binding=stream`); + +try { + for await (const delivery of client.listen(AXME_AGENT_ADDRESS)) { + const intentId = String(delivery.intent_id ?? ""); + if (!intentId) continue; + console.log(`received intent ${intentId}`); + + let intent: Record; + try { + const resp = await client.getIntent(intentId); + intent = (resp.intent ?? resp) as Record; + } catch (e) { console.error(`get_intent(${intentId}) failed:`, e); continue; } + + const status = String(intent.lifecycle_status ?? intent.status ?? "").toUpperCase(); + if (!["CREATED", "DELIVERED", "ACKNOWLEDGED", "IN_PROGRESS", "WAITING"].includes(status)) continue; + + const rawPayload = (intent.payload ?? {}) as Record; + const effectivePayload = (rawPayload.parent_payload ?? rawPayload) as Record; + const result = processWebhook(effectivePayload); + + console.log(`processed webhook for ${intentId}: fingerprint=${result.fingerprint}`); + + try { + await client.resumeIntent(intentId, { action: "complete", ...result }, { ownerAgent: AXME_AGENT_ADDRESS }); + console.log(`resumed intent ${intentId}`); + } catch (e) { console.error(`resume_intent(${intentId}) failed:`, e); } + } +} catch (e) { + if ((e as Error).name !== "AbortError") throw e; +} diff --git a/examples/durability/timeout/dotnet/TimeoutAgent.cs b/examples/durability/timeout/dotnet/TimeoutAgent.cs new file mode 100644 index 0000000..e38789a --- /dev/null +++ b/examples/durability/timeout/dotnet/TimeoutAgent.cs @@ -0,0 +1,45 @@ +using System.Text.Json.Nodes; +using Axme.Sdk; +using AxmeExamples; + +namespace AxmeExamples.Durability; + +public class TimeoutAgent +{ + public static async Task Main(string[] args) + { + var baseUrl = SseHelper.Env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + var apiKey = SseHelper.Env("AXME_API_KEY"); + var addr = SseHelper.Env("AXME_AGENT_ADDRESS", "slow-batch-processor"); + if (string.IsNullOrEmpty(apiKey)) { Console.Error.WriteLine("AXME_API_KEY is required"); return; } + + var client = new AxmeClient(new AxmeClientConfig { ApiKey = apiKey, BaseUrl = baseUrl }); + Console.WriteLine($"slow-batch-processor starting address={addr}"); + var since = 0; + + while (true) + { + try + { + foreach (var d in await SseHelper.PollAgentStreamAsync(baseUrl, apiKey, addr, since)) + { + var id = SseHelper.Str(d, "intent_id"); + if (d["seq"]?.GetValue() is int seq) since = Math.Max(since, seq); + if (string.IsNullOrEmpty(id)) continue; + var intent = SseHelper.UnwrapIntent(await client.GetIntentAsync(id)); + if (!SseHelper.IsActionable(intent)) continue; + var p = SseHelper.EffectivePayload(intent); + + var batchId = SseHelper.Str(p, "batch_id", "?"); + var rc = (int)(p["record_count"]?.GetValue() ?? 0); + JsonObject res; + if (rc > 10000) res = new JsonObject { ["action"] = "fail", ["batch_id"] = batchId, ["error"] = $"batch too large: {rc}" }; + else res = new JsonObject { ["action"] = "complete", ["batch_id"] = batchId, ["record_count"] = rc, ["processed"] = true }; + await client.ResumeIntentAsync(id, res, new RequestOptions { OwnerAgent = addr }); + Console.WriteLine($"resumed {id} action={res["action"]}"); + } + } + catch { await Task.Delay(2000); } + } + } +} diff --git a/examples/durability/timeout/go/agent.go b/examples/durability/timeout/go/agent.go new file mode 100644 index 0000000..2525ada --- /dev/null +++ b/examples/durability/timeout/go/agent.go @@ -0,0 +1,70 @@ +// Slow Batch Processor — durability/timeout example (Go). +// +// Run: +// +// export AXME_API_KEY= +// export AXME_AGENT_ADDRESS=slow-batch-processor +// go run examples/durability/timeout/agent.go +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "strings" + + "github.com/AxmeAI/axme-sdk-go/axme" +) + +var ( + baseURL = envOr("AXME_BASE_URL", "https://api.cloud.axme.ai") + apiKey = os.Getenv("AXME_API_KEY") + agentAddress = envOr("AXME_AGENT_ADDRESS", "slow-batch-processor") +) + +func envOr(k, d string) string { if v := os.Getenv(k); v != "" { return v }; return d } +func str(m map[string]any, k, d string) string { if v, ok := m[k].(string); ok { return v }; return d } + +func processBatch(p map[string]any) (bool, map[string]any) { + batchID := str(p, "batch_id", "?") + rc := 0; if f, ok := p["record_count"].(float64); ok { rc = int(f) } + if rc > 10000 { + return false, map[string]any{"batch_id": batchID, "error": fmt.Sprintf("batch too large: %d records exceeds limit of 10000", rc), "record_count": rc} + } + return true, map[string]any{"batch_id": batchID, "record_count": rc, "processed": true} +} + +func main() { + if apiKey == "" { log.Fatal("AXME_API_KEY is required") } + client, err := axme.NewClient(axme.ClientConfig{APIKey: apiKey, BaseURL: baseURL}) + if err != nil { log.Fatalf("client init: %v", err) } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + log.Printf("slow-batch-processor starting address=%s binding=stream", agentAddress) + + intents, errs := client.Listen(ctx, agentAddress, axme.ListenOptions{}) + for { + select { + case d, ok := <-intents: + if !ok { return } + id := str(d, "intent_id", ""); if id == "" { continue } + resp, _ := client.GetIntent(ctx, id, axme.RequestOptions{}) + intent, _ := resp["intent"].(map[string]any); if intent == nil { intent = resp } + status := strings.ToUpper(str(intent, "lifecycle_status", str(intent, "status", ""))) + if !map[string]bool{"CREATED": true, "DELIVERED": true, "ACKNOWLEDGED": true, "IN_PROGRESS": true, "WAITING": true}[status] { continue } + raw, _ := intent["payload"].(map[string]any); if raw == nil { raw = map[string]any{} } + eff, _ := raw["parent_payload"].(map[string]any); if eff == nil { eff = raw } + + ok2, result := processBatch(eff) + action := "complete"; if !ok2 { action = "fail" } + result["action"] = action + client.ResumeIntent(ctx, id, result, axme.RequestOptions{OwnerAgent: agentAddress}) + log.Printf("resumed %s action=%s", id, action) + case err, ok := <-errs: if !ok { return }; log.Printf("error: %v", err) + case <-ctx.Done(): return + } + } +} diff --git a/examples/durability/timeout/java/TimeoutAgent.java b/examples/durability/timeout/java/TimeoutAgent.java new file mode 100644 index 0000000..027f6e8 --- /dev/null +++ b/examples/durability/timeout/java/TimeoutAgent.java @@ -0,0 +1,38 @@ +package ai.axme.examples.durability; + +import ai.axme.examples.SseHelper; +import dev.axme.sdk.*; +import java.util.*; + +/** Slow Batch Processor — durability/timeout example (Java). */ +public class TimeoutAgent { + static final String BASE = SseHelper.env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + static final String KEY = System.getenv("AXME_API_KEY"); + static final String ADDR = SseHelper.env("AXME_AGENT_ADDRESS", "slow-batch-processor"); + + public static void main(String[] args) throws Exception { + if (KEY == null || KEY.isEmpty()) { System.err.println("AXME_API_KEY is required"); System.exit(1); } + AxmeClient client = new AxmeClient(new AxmeClientConfig(BASE, KEY)); + System.out.printf("slow-batch-processor starting address=%s%n", ADDR); + int since = 0; + while (true) { + try { + for (Map d : SseHelper.pollAgentStream(BASE, KEY, ADDR, since, 15)) { + String id = SseHelper.str(d, "intent_id"); Number seq = (Number) d.get("seq"); + if (seq != null) since = Math.max(since, seq.intValue()); if (id.isEmpty()) continue; + Map intent = SseHelper.unwrapIntent(client.getIntent(id, RequestOptions.none())); + if (!SseHelper.isActionable(intent)) continue; + Map p = SseHelper.effectivePayload(intent); + + String batchId = SseHelper.str(p, "batch_id", "?"); + int rc = p.get("record_count") instanceof Number n ? n.intValue() : 0; + Map res = new HashMap<>(); + if (rc > 10000) { res.put("action", "fail"); res.put("batch_id", batchId); res.put("error", "batch too large: " + rc); } + else { res.put("action", "complete"); res.put("batch_id", batchId); res.put("record_count", rc); res.put("processed", true); } + client.resumeIntent(id, res, SseHelper.withOwner(ADDR)); + System.out.printf("resumed %s action=%s%n", id, res.get("action")); + } + } catch (Exception e) { Thread.sleep(2000); } + } + } +} diff --git a/examples/durability/timeout/agent.py b/examples/durability/timeout/python/agent.py similarity index 100% rename from examples/durability/timeout/agent.py rename to examples/durability/timeout/python/agent.py diff --git a/examples/durability/timeout/typescript/agent.ts b/examples/durability/timeout/typescript/agent.ts new file mode 100644 index 0000000..065b7b3 --- /dev/null +++ b/examples/durability/timeout/typescript/agent.ts @@ -0,0 +1,63 @@ +/** + * Slow Batch Processor — durability/timeout example (TypeScript). + * + * Demonstrates step deadline enforcement. The scenario sets step_deadline_seconds=10. + * Run WITHOUT the agent to observe the timeout behavior. + * + * Run: + * export AXME_API_KEY= + * export AXME_AGENT_ADDRESS=slow-batch-processor + * npx tsx examples/durability/timeout/agent.ts + */ +import { AxmeClient } from "@axme/axme"; + +const AXME_BASE_URL = process.env.AXME_BASE_URL ?? "https://api.cloud.axme.ai"; +const AXME_API_KEY = process.env.AXME_API_KEY ?? ""; +const AXME_AGENT_ADDRESS = process.env.AXME_AGENT_ADDRESS ?? "slow-batch-processor"; + +if (!AXME_API_KEY) { console.error("AXME_API_KEY is required"); process.exit(1); } + +const MAX_BATCH_RECORDS = 10_000; + +function processBatch(payload: Record): { ok: boolean; result: Record } { + const batchId = String(payload.batch_id ?? "?"); + const recordCount = Number(payload.record_count ?? 0); + + if (recordCount > MAX_BATCH_RECORDS) { + return { ok: false, result: { batch_id: batchId, error: `batch too large: ${recordCount} records exceeds limit of ${MAX_BATCH_RECORDS}`, record_count: recordCount } }; + } + return { ok: true, result: { batch_id: batchId, record_count: recordCount, processed: true } }; +} + +const client = new AxmeClient({ apiKey: AXME_API_KEY, baseUrl: AXME_BASE_URL }); +console.log(`slow-batch-processor starting address=${AXME_AGENT_ADDRESS} binding=stream`); + +try { + for await (const delivery of client.listen(AXME_AGENT_ADDRESS)) { + const intentId = String(delivery.intent_id ?? ""); + if (!intentId) continue; + console.log(`received intent ${intentId}`); + + let intent: Record; + try { + const resp = await client.getIntent(intentId); + intent = (resp.intent ?? resp) as Record; + } catch (e) { console.error(`get_intent(${intentId}) failed:`, e); continue; } + + const status = String(intent.lifecycle_status ?? intent.status ?? "").toUpperCase(); + if (!["CREATED", "DELIVERED", "ACKNOWLEDGED", "IN_PROGRESS", "WAITING"].includes(status)) continue; + + const rawPayload = (intent.payload ?? {}) as Record; + const effectivePayload = (rawPayload.parent_payload ?? rawPayload) as Record; + const { ok, result } = processBatch(effectivePayload); + + console.log(`batch result for ${intentId}: ok=${ok}`); + + try { + await client.resumeIntent(intentId, { action: ok ? "complete" : "fail", ...result }, { ownerAgent: AXME_AGENT_ADDRESS }); + console.log(`resumed intent ${intentId}`); + } catch (e) { console.error(`resume_intent(${intentId}) failed:`, e); } + } +} catch (e) { + if ((e as Error).name !== "AbortError") throw e; +} diff --git a/examples/full/multi-agent/dotnet/MultiAgentRunner.cs b/examples/full/multi-agent/dotnet/MultiAgentRunner.cs new file mode 100644 index 0000000..ca14411 --- /dev/null +++ b/examples/full/multi-agent/dotnet/MultiAgentRunner.cs @@ -0,0 +1,19 @@ +// Multi-Agent Runner (.NET) — starts both agents for the full scenario. +// In practice, each agent runs as a separate process. +// This file documents the pattern. +using AxmeExamples; + +namespace AxmeExamples.Full; + +public class MultiAgentRunner +{ + public static void Main(string[] args) + { + Console.WriteLine("Starting agents for full/multi-agent scenario...\n"); + Console.WriteLine("In .NET, run each agent as a separate process:"); + Console.WriteLine(" AXME_API_KEY= AXME_AGENT_ADDRESS=compliance-checker-agent dotnet run --project examples/dotnet -- Delivery/StreamAgent"); + Console.WriteLine(" AXME_API_KEY= AXME_AGENT_ADDRESS=risk-assessment-agent dotnet run --project examples/dotnet -- Internal/NotificationAgent"); + Console.WriteLine("\nThen run the scenario:"); + Console.WriteLine(" axme scenarios apply examples/full/multi-agent/scenario.json --watch"); + } +} diff --git a/examples/full/multi-agent/go/run_agents.go b/examples/full/multi-agent/go/run_agents.go new file mode 100644 index 0000000..cbf87ff --- /dev/null +++ b/examples/full/multi-agent/go/run_agents.go @@ -0,0 +1,92 @@ +// Run both agents for the full multi-agent scenario (Go). +// +// Usage: +// +// go run examples/full/multi-agent/run_agents.go +package main + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "os/signal" + "path/filepath" + "runtime" +) + +type agentSpec struct { + nameFragment string + script string + address string +} + +func loadAPIKey(nameFragment string) string { + home, _ := os.UserHomeDir() + data, err := os.ReadFile(filepath.Join(home, ".config", "axme", "scenario-agents.json")) + if err != nil { return "" } + var raw map[string]any + json.Unmarshal(data, &raw) + agents, _ := raw["agents"].([]any) + for _, a := range agents { + m, _ := a.(map[string]any) + addr, _ := m["address"].(string) + key, _ := m["api_key"].(string) + if len(addr) > 0 && len(key) > 0 { + if contains(addr, nameFragment) { return key } + } + } + return "" +} + +func contains(s, sub string) bool { + return len(s) >= len(sub) && (s == sub || len(sub) == 0 || findSubstring(s, sub)) +} +func findSubstring(s, sub string) bool { + for i := 0; i <= len(s)-len(sub); i++ { + if s[i:i+len(sub)] == sub { return true } + } + return false +} + +func main() { + _, thisFile, _, _ := runtime.Caller(0) + workspace := filepath.Join(filepath.Dir(thisFile), "..", "..", "..") + + agents := []agentSpec{ + {"compliance-checker", filepath.Join(workspace, "examples", "delivery", "stream", "agent.go"), "compliance-checker-agent"}, + {"risk-assessment", filepath.Join(workspace, "examples", "internal", "notification", "agent.go"), "risk-assessment-agent"}, + } + + fmt.Println("Starting agents for full/multi-agent scenario...") + fmt.Println() + + var procs []*exec.Cmd + for _, a := range agents { + key := loadAPIKey(a.nameFragment) + if key == "" { key = os.Getenv("AXME_API_KEY") } + if key == "" { fmt.Fprintf(os.Stderr, "[warn] no API key for '%s'\n", a.nameFragment) } + + env := append(os.Environ(), "AXME_AGENT_ADDRESS="+a.address) + if key != "" { env = append(env, "AXME_API_KEY="+key) } + + fmt.Printf("[agent] %s — %s\n", a.address, filepath.Base(a.script)) + cmd := exec.Command("go", "run", a.script) + cmd.Env = env + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Start() + procs = append(procs, cmd) + } + + fmt.Println("\nBoth agents running. Press Ctrl+C to stop.") + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + <-c + + fmt.Println("\nShutting down agents...") + for _, p := range procs { + if p.Process != nil { p.Process.Kill() } + } +} diff --git a/examples/full/multi-agent/java/MultiAgentRunner.java b/examples/full/multi-agent/java/MultiAgentRunner.java new file mode 100644 index 0000000..28c5330 --- /dev/null +++ b/examples/full/multi-agent/java/MultiAgentRunner.java @@ -0,0 +1,65 @@ +package ai.axme.examples.full; + +import ai.axme.examples.SseHelper; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.File; +import java.util.*; + +/** Run both agents for the full multi-agent scenario (Java). */ +public class MultiAgentRunner { + public static void main(String[] args) throws Exception { + System.out.println("Starting agents for full/multi-agent scenario...\n"); + // In Java, running multiple agent mains requires separate processes or threads. + // Here we use threads for simplicity — each agent's main runs in its own thread. + + String[][] agents = { + {"compliance-checker", "ai.axme.examples.delivery.StreamAgent", "compliance-checker-agent"}, + {"risk-assessment", "ai.axme.examples.internal.NotificationAgent", "risk-assessment-agent"}, + }; + + List threads = new ArrayList<>(); + for (String[] a : agents) { + String key = loadApiKey(a[0]); + if (key.isEmpty()) key = System.getenv("AXME_API_KEY"); + if (key == null || key.isEmpty()) { + System.err.printf("[warn] no API key for '%s'%n", a[0]); + continue; + } + + // Set env vars via system properties (agent code reads System.getenv) + // Note: System.setenv is not possible in Java; agents should read from a shared config + System.out.printf("[agent] %s — %s%n", a[2], a[1]); + String finalKey = key; + Thread t = new Thread(() -> { + try { + // For a real multi-agent setup, launch separate JVM processes. + // This demo shows the pattern — real deployment uses separate processes. + System.out.printf("Agent %s would start with key=%s...%n", a[2], finalKey.substring(0, 10) + "..."); + } catch (Exception e) { e.printStackTrace(); } + }, a[2]); + t.setDaemon(true); + t.start(); + threads.add(t); + } + + System.out.println("\nBoth agents configured. In production, run each agent as a separate process."); + System.out.println("Example:"); + System.out.println(" AXME_API_KEY= AXME_AGENT_ADDRESS=compliance-checker-agent mvn exec:java -Dexec.mainClass=ai.axme.examples.delivery.StreamAgent"); + System.out.println(" AXME_API_KEY= AXME_AGENT_ADDRESS=risk-assessment-agent mvn exec:java -Dexec.mainClass=ai.axme.examples.internal.NotificationAgent"); + } + + @SuppressWarnings("unchecked") + static String loadApiKey(String nameFragment) { + try { + String home = System.getProperty("user.home"); + File f = new File(home, ".config/axme/scenario-agents.json"); + Map data = new ObjectMapper().readValue(f, Map.class); + for (Map agent : (List>) data.getOrDefault("agents", List.of())) { + String addr = (String) agent.getOrDefault("address", ""); + if (addr.contains(nameFragment)) return (String) agent.getOrDefault("api_key", ""); + } + } catch (Exception ignored) {} + return ""; + } +} diff --git a/examples/full/multi-agent/run_agents.py b/examples/full/multi-agent/python/run_agents.py similarity index 100% rename from examples/full/multi-agent/run_agents.py rename to examples/full/multi-agent/python/run_agents.py diff --git a/examples/full/multi-agent/typescript/run_agents.ts b/examples/full/multi-agent/typescript/run_agents.ts new file mode 100644 index 0000000..1b0c761 --- /dev/null +++ b/examples/full/multi-agent/typescript/run_agents.ts @@ -0,0 +1,65 @@ +/** + * Run both agents for the full multi-agent scenario (TypeScript). + * + * Starts compliance-checker-agent and risk-assessment-agent concurrently. + * + * Usage: + * npx tsx examples/full/multi-agent/run_agents.ts + * + * Then in another terminal: + * axme scenarios apply examples/full/multi-agent/scenario.json --watch + */ +import { spawn, ChildProcess } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { dirname } from "node:path"; + +const __filename = fileURLToPath(import.meta.url); +const WORKSPACE = join(dirname(__filename), "..", "..", ".."); +const AGENT_CREDS = join(homedir(), ".config", "axme", "scenario-agents.json"); + +const AGENTS = [ + { nameFragment: "compliance-checker", script: join(WORKSPACE, "examples", "delivery", "stream", "agent.ts"), address: "compliance-checker-agent" }, + { nameFragment: "risk-assessment", script: join(WORKSPACE, "examples", "internal", "notification", "agent.ts"), address: "risk-assessment-agent" }, +]; + +function loadApiKey(nameFragment: string): string { + try { + const data = JSON.parse(readFileSync(AGENT_CREDS, "utf-8")); + for (const entry of data.agents ?? []) { + if (String(entry.address ?? "").includes(nameFragment)) return String(entry.api_key ?? ""); + } + } catch { /* ignore */ } + return ""; +} + +console.log("Starting agents for full/multi-agent scenario...\n"); + +const procs: ChildProcess[] = []; + +for (const agent of AGENTS) { + let key = loadApiKey(agent.nameFragment); + if (!key) key = process.env.AXME_API_KEY ?? ""; + if (!key) { + console.warn(`[warn] no API key found for '${agent.nameFragment}'; run 'axme scenarios apply ... --watch' first`); + } + + const env = { ...process.env, AXME_AGENT_ADDRESS: agent.address }; + if (key) env.AXME_API_KEY = key; + + console.log(`[agent] ${agent.address} — ${agent.script.split("/").pop()}`); + const proc = spawn("npx", ["tsx", agent.script], { env, stdio: "inherit" }); + procs.push(proc); +} + +console.log("\nBoth agents running. Press Ctrl+C to stop.\n"); + +process.on("SIGINT", () => { + console.log("\nShutting down agents..."); + for (const proc of procs) proc.kill("SIGTERM"); + process.exit(0); +}); + +await Promise.all(procs.map(p => new Promise(r => p.on("exit", r)))); diff --git a/examples/human/cli/dotnet/CliAgent.cs b/examples/human/cli/dotnet/CliAgent.cs new file mode 100644 index 0000000..d8e90d9 --- /dev/null +++ b/examples/human/cli/dotnet/CliAgent.cs @@ -0,0 +1,55 @@ +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; +using Axme.Sdk; +using AxmeExamples; + +namespace AxmeExamples.Human; + +public class CliAgent +{ + public static async Task Main(string[] args) + { + var baseUrl = SseHelper.Env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + var apiKey = SseHelper.Env("AXME_API_KEY"); + var addr = SseHelper.Env("AXME_AGENT_ADDRESS", "deploy-readiness-checker"); + if (string.IsNullOrEmpty(apiKey)) { Console.Error.WriteLine("AXME_API_KEY is required"); return; } + + var client = new AxmeClient(new AxmeClientConfig { ApiKey = apiKey, BaseUrl = baseUrl }); + Console.WriteLine($"deploy-readiness-checker starting address={addr}"); + var semver = new Regex(@"^\d+\.\d+\.\d+$"); + string[] envs = ["staging", "canary", "preview"]; + var since = 0; + + while (true) + { + try + { + foreach (var d in await SseHelper.PollAgentStreamAsync(baseUrl, apiKey, addr, since)) + { + var id = SseHelper.Str(d, "intent_id"); + if (d["seq"]?.GetValue() is int seq) since = Math.Max(since, seq); + if (string.IsNullOrEmpty(id)) continue; + var intent = SseHelper.UnwrapIntent(await client.GetIntentAsync(id)); + if (!SseHelper.IsActionable(intent)) continue; + var p = SseHelper.EffectivePayload(intent); + + var ver = SseHelper.Str(p, "version"); var env = SseHelper.Str(p, "environment"); + var rb = SseHelper.Str(p, "rollback_tag"); var svc = SseHelper.Str(p, "service"); + var passed = new List(); var fail = new List(); + if (semver.IsMatch(ver)) passed.Add("version ok"); else fail.Add("version invalid"); + if (envs.Contains(env)) passed.Add("env ok"); else fail.Add("env not allowed"); + if (!string.IsNullOrEmpty(rb)) passed.Add("rollback set"); else fail.Add("rollback missing"); + if (!string.IsNullOrEmpty(svc)) passed.Add("service set"); else fail.Add("service empty"); + var ready = fail.Count == 0; + + var res = new JsonObject { ["action"] = ready ? "complete" : "fail", ["ready"] = ready, + ["passed_checks"] = new JsonArray(passed.Select(s => (JsonNode)JsonValue.Create(s)!).ToArray()), + ["failures"] = new JsonArray(fail.Select(s => (JsonNode)JsonValue.Create(s)!).ToArray()) }; + await client.ResumeIntentAsync(id, res, new RequestOptions { OwnerAgent = addr }); + Console.WriteLine($"resumed {id} (ready={ready})"); + } + } + catch { await Task.Delay(2000); } + } + } +} diff --git a/examples/human/cli/go/agent.go b/examples/human/cli/go/agent.go new file mode 100644 index 0000000..3339086 --- /dev/null +++ b/examples/human/cli/go/agent.go @@ -0,0 +1,84 @@ +// Deploy Readiness Checker — human/cli example (Go). +// +// Run: +// +// export AXME_API_KEY= +// export AXME_AGENT_ADDRESS=deploy-readiness-checker +// go run examples/human/cli/agent.go +package main + +import ( + "context" + "log" + "os" + "os/signal" + "regexp" + "strings" + + "github.com/AxmeAI/axme-sdk-go/axme" +) + +var ( + baseURL = envOr("AXME_BASE_URL", "https://api.cloud.axme.ai") + apiKey = os.Getenv("AXME_API_KEY") + agentAddress = envOr("AXME_AGENT_ADDRESS", "deploy-readiness-checker") + semverRe = regexp.MustCompile(`^\d+\.\d+\.\d+$`) + allowedEnvs = map[string]bool{"staging": true, "canary": true, "preview": true} +) + +func envOr(k, d string) string { if v := os.Getenv(k); v != "" { return v }; return d } +func str(m map[string]any, k, d string) string { if v, ok := m[k].(string); ok { return v }; return d } + +func checkReadiness(p map[string]any) (bool, []string, []string) { + version := str(p, "version", "") + env := str(p, "environment", "") + rollback := str(p, "rollback_tag", "") + service := str(p, "service", "") + var passed, failures []string + + if semverRe.MatchString(version) { passed = append(passed, "version '"+version+"' is valid semver") } else { failures = append(failures, "version '"+version+"' is not valid semver") } + if allowedEnvs[env] { passed = append(passed, "environment '"+env+"' is in allowed list") } else { failures = append(failures, "environment '"+env+"' not in allowed set") } + if rollback != "" { passed = append(passed, "rollback_tag '"+rollback+"' is set") } else { failures = append(failures, "rollback_tag is missing") } + if service != "" { passed = append(passed, "service '"+service+"' is identified") } else { failures = append(failures, "service name is empty") } + + return len(failures) == 0, passed, failures +} + +func main() { + if apiKey == "" { log.Fatal("AXME_API_KEY is required") } + client, err := axme.NewClient(axme.ClientConfig{APIKey: apiKey, BaseURL: baseURL}) + if err != nil { log.Fatalf("client init: %v", err) } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + log.Printf("deploy-readiness-checker starting address=%s binding=stream", agentAddress) + + intents, errs := client.Listen(ctx, agentAddress, axme.ListenOptions{}) + for { + select { + case d, ok := <-intents: + if !ok { return } + id := str(d, "intent_id", ""); if id == "" { continue } + log.Printf("received intent %s", id) + + resp, err := client.GetIntent(ctx, id, axme.RequestOptions{}) + if err != nil { log.Printf("get_intent(%s) failed: %v", id, err); continue } + intent, _ := resp["intent"].(map[string]any); if intent == nil { intent = resp } + status := strings.ToUpper(str(intent, "lifecycle_status", str(intent, "status", ""))) + if !map[string]bool{"CREATED": true, "DELIVERED": true, "ACKNOWLEDGED": true, "IN_PROGRESS": true, "WAITING": true}[status] { continue } + + raw, _ := intent["payload"].(map[string]any); if raw == nil { raw = map[string]any{} } + eff, _ := raw["parent_payload"].(map[string]any); if eff == nil { eff = raw } + + ready, passed, failures := checkReadiness(eff) + log.Printf("readiness for %s: ready=%v passed=%d failed=%d", id, ready, len(passed), len(failures)) + + var payload map[string]any + if ready { payload = map[string]any{"action": "complete", "ready": true, "passed_checks": passed, "failures": []string{}} } else { payload = map[string]any{"action": "fail", "ready": false, "passed_checks": passed, "failures": failures} } + if _, err = client.ResumeIntent(ctx, id, payload, axme.RequestOptions{OwnerAgent: agentAddress}); err != nil { log.Printf("resume failed: %v", err) } + log.Printf("resumed %s (ready=%v)", id, ready) + case err, ok := <-errs: if !ok { return }; log.Printf("error: %v", err) + case <-ctx.Done(): return + } + } +} diff --git a/examples/human/cli/java/CliAgent.java b/examples/human/cli/java/CliAgent.java new file mode 100644 index 0000000..f4d4aec --- /dev/null +++ b/examples/human/cli/java/CliAgent.java @@ -0,0 +1,48 @@ +package ai.axme.examples.human; + +import ai.axme.examples.SseHelper; +import dev.axme.sdk.*; +import java.util.*; +import java.util.regex.Pattern; + +/** Deploy Readiness Checker — human/cli example (Java). */ +public class CliAgent { + static final String BASE = SseHelper.env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + static final String KEY = System.getenv("AXME_API_KEY"); + static final String ADDR = SseHelper.env("AXME_AGENT_ADDRESS", "deploy-readiness-checker"); + static final Pattern SEMVER = Pattern.compile("^\\d+\\.\\d+\\.\\d+$"); + static final Set ENVS = Set.of("staging", "canary", "preview"); + + public static void main(String[] args) throws Exception { + if (KEY == null || KEY.isEmpty()) { System.err.println("AXME_API_KEY is required"); System.exit(1); } + AxmeClient client = new AxmeClient(new AxmeClientConfig(BASE, KEY)); + System.out.printf("deploy-readiness-checker starting address=%s%n", ADDR); + int since = 0; + while (true) { + try { + for (Map d : SseHelper.pollAgentStream(BASE, KEY, ADDR, since, 15)) { + String id = SseHelper.str(d, "intent_id"); Number seq = (Number) d.get("seq"); + if (seq != null) since = Math.max(since, seq.intValue()); if (id.isEmpty()) continue; + Map intent = SseHelper.unwrapIntent(client.getIntent(id, RequestOptions.none())); + if (!SseHelper.isActionable(intent)) continue; + Map p = SseHelper.effectivePayload(intent); + + String ver = SseHelper.str(p, "version"), env = SseHelper.str(p, "environment"); + String rb = SseHelper.str(p, "rollback_tag"), svc = SseHelper.str(p, "service"); + List passed = new ArrayList<>(), fail = new ArrayList<>(); + if (SEMVER.matcher(ver).matches()) passed.add("version ok"); else fail.add("version invalid"); + if (ENVS.contains(env)) passed.add("env ok"); else fail.add("env not allowed"); + if (!rb.isEmpty()) passed.add("rollback set"); else fail.add("rollback missing"); + if (!svc.isEmpty()) passed.add("service set"); else fail.add("service empty"); + + boolean ready = fail.isEmpty(); + Map res = new HashMap<>(); + res.put("action", ready ? "complete" : "fail"); res.put("ready", ready); + res.put("passed_checks", passed); res.put("failures", fail); + client.resumeIntent(id, res, SseHelper.withOwner(ADDR)); + System.out.printf("resumed %s (ready=%s)%n", id, ready); + } + } catch (Exception e) { Thread.sleep(2000); } + } + } +} diff --git a/examples/human/cli/agent.py b/examples/human/cli/python/agent.py similarity index 100% rename from examples/human/cli/agent.py rename to examples/human/cli/python/agent.py diff --git a/examples/human/cli/typescript/agent.ts b/examples/human/cli/typescript/agent.ts new file mode 100644 index 0000000..e750572 --- /dev/null +++ b/examples/human/cli/typescript/agent.ts @@ -0,0 +1,83 @@ +/** + * Deploy Readiness Checker — human/cli example (TypeScript). + * + * Validates deployment readiness before routing to a human operator. + * After the agent step, the workflow pauses for human approval via CLI. + * + * Run: + * export AXME_API_KEY= + * export AXME_AGENT_ADDRESS=deploy-readiness-checker + * npx tsx examples/human/cli/agent.ts + */ +import { AxmeClient } from "@axme/axme"; + +const AXME_BASE_URL = process.env.AXME_BASE_URL ?? "https://api.cloud.axme.ai"; +const AXME_API_KEY = process.env.AXME_API_KEY ?? ""; +const AXME_AGENT_ADDRESS = process.env.AXME_AGENT_ADDRESS ?? "deploy-readiness-checker"; + +if (!AXME_API_KEY) { console.error("AXME_API_KEY is required"); process.exit(1); } + +// --- Business logic --- +const SEMVER_RE = /^\d+\.\d+\.\d+$/; +const ALLOWED_ENVIRONMENTS = new Set(["staging", "canary", "preview"]); + +function checkReadiness(payload: Record): { ready: boolean; passed: string[]; failures: string[] } { + const version = String(payload.version ?? ""); + const environment = String(payload.environment ?? ""); + const rollback = String(payload.rollback_tag ?? ""); + const service = String(payload.service ?? ""); + const passed: string[] = []; + const failures: string[] = []; + + if (SEMVER_RE.test(version)) passed.push(`version '${version}' is valid semver`); + else failures.push(`version '${version}' is not valid semver (expected MAJOR.MINOR.PATCH)`); + + if (ALLOWED_ENVIRONMENTS.has(environment)) passed.push(`environment '${environment}' is in allowed list`); + else failures.push(`environment '${environment}' not in allowed set: ${[...ALLOWED_ENVIRONMENTS].sort()}`); + + if (rollback) passed.push(`rollback_tag '${rollback}' is set`); + else failures.push("rollback_tag is missing — deployment must have a rollback path"); + + if (service) passed.push(`service '${service}' is identified`); + else failures.push("service name is empty"); + + return { ready: failures.length === 0, passed, failures }; +} + +// --- Agent loop --- +const client = new AxmeClient({ apiKey: AXME_API_KEY, baseUrl: AXME_BASE_URL }); +console.log(`deploy-readiness-checker starting address=${AXME_AGENT_ADDRESS} binding=stream`); + +try { + for await (const delivery of client.listen(AXME_AGENT_ADDRESS)) { + const intentId = String(delivery.intent_id ?? ""); + if (!intentId) continue; + console.log(`received intent ${intentId}`); + + let intent: Record; + try { + const resp = await client.getIntent(intentId); + intent = (resp.intent ?? resp) as Record; + } catch (e) { console.error(`get_intent(${intentId}) failed:`, e); continue; } + + const status = String(intent.lifecycle_status ?? intent.status ?? "").toUpperCase(); + if (!["CREATED", "DELIVERED", "ACKNOWLEDGED", "IN_PROGRESS", "WAITING"].includes(status)) continue; + + const rawPayload = (intent.payload ?? {}) as Record; + const effectivePayload = (rawPayload.parent_payload ?? rawPayload) as Record; + const { ready, passed, failures } = checkReadiness(effectivePayload); + + console.log(`readiness for ${intentId}: ready=${ready} passed=${passed.length} failed=${failures.length}`); + + try { + if (ready) { + await client.resumeIntent(intentId, { action: "complete", ready: true, passed_checks: passed, failures: [] }, { ownerAgent: AXME_AGENT_ADDRESS }); + } else { + await client.resumeIntent(intentId, { action: "fail", ready: false, passed_checks: passed, failures }, { ownerAgent: AXME_AGENT_ADDRESS }); + } + console.log(`resumed intent ${intentId} (ready=${ready})`); + } catch (e) { console.error(`resume_intent(${intentId}) failed:`, e); } + } +} catch (e) { + if ((e as Error).name !== "AbortError") throw e; +} diff --git a/examples/human/email/dotnet/EmailAgent.cs b/examples/human/email/dotnet/EmailAgent.cs new file mode 100644 index 0000000..7b1e6a9 --- /dev/null +++ b/examples/human/email/dotnet/EmailAgent.cs @@ -0,0 +1,57 @@ +using System.Text.Json.Nodes; +using Axme.Sdk; +using AxmeExamples; + +namespace AxmeExamples.Human; + +public class EmailAgent +{ + public static async Task Main(string[] args) + { + var baseUrl = SseHelper.Env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + var apiKey = SseHelper.Env("AXME_API_KEY"); + var addr = SseHelper.Env("AXME_AGENT_ADDRESS", "budget-envelope-validator"); + if (string.IsNullOrEmpty(apiKey)) { Console.Error.WriteLine("AXME_API_KEY is required"); return; } + + var client = new AxmeClient(new AxmeClientConfig { ApiKey = apiKey, BaseUrl = baseUrl }); + Console.WriteLine($"budget-envelope-validator starting address={addr}"); + var envelopes = new Dictionary { ["cloud_infrastructure"] = 50000, ["software_licenses"] = 20000, ["contractor_services"] = 40000, ["hardware"] = 15000 }; + var since = 0; + + while (true) + { + try + { + foreach (var d in await SseHelper.PollAgentStreamAsync(baseUrl, apiKey, addr, since)) + { + var id = SseHelper.Str(d, "intent_id"); + if (d["seq"]?.GetValue() is int seq) since = Math.Max(since, seq); + if (string.IsNullOrEmpty(id)) continue; + var intent = SseHelper.UnwrapIntent(await client.GetIntentAsync(id)); + if (!SseHelper.IsActionable(intent)) continue; + var p = SseHelper.EffectivePayload(intent); + + var amount = p["amount"]?.GetValue() ?? 0; + var category = SseHelper.Str(p, "category"); + var currency = SseHelper.Str(p, "currency", "USD"); + + JsonObject res; + if (!envelopes.TryGetValue(category, out var envelope)) + res = new JsonObject { ["action"] = "fail", ["within_envelope"] = false, ["reason"] = "unknown category" }; + else if (amount > envelope) + res = new JsonObject { ["action"] = "fail", ["within_envelope"] = false, ["reason"] = "exceeds envelope" }; + else + { + var headroom = Math.Round((envelope - amount) / envelope * 1000) / 10; + res = new JsonObject { ["action"] = "complete", ["within_envelope"] = true, + ["reason"] = $"{currency} {amount:N0} within {category} ({headroom}% headroom)", + ["envelope"] = envelope, ["headroom_pct"] = headroom }; + } + await client.ResumeIntentAsync(id, res, new RequestOptions { OwnerAgent = addr }); + Console.WriteLine($"resumed {id} (within_envelope={res["within_envelope"]})"); + } + } + catch { await Task.Delay(2000); } + } + } +} diff --git a/examples/human/email/go/agent.go b/examples/human/email/go/agent.go new file mode 100644 index 0000000..04c1a21 --- /dev/null +++ b/examples/human/email/go/agent.go @@ -0,0 +1,100 @@ +// Budget Envelope Validator — human/email example (Go). +// +// Run: +// +// export AXME_API_KEY= +// export AXME_AGENT_ADDRESS=budget-envelope-validator +// go run examples/human/email/agent.go +package main + +import ( + "context" + "fmt" + "log" + "math" + "os" + "os/signal" + "sort" + "strings" + + "github.com/AxmeAI/axme-sdk-go/axme" +) + +var ( + baseURL = envOr("AXME_BASE_URL", "https://api.cloud.axme.ai") + apiKey = os.Getenv("AXME_API_KEY") + agentAddress = envOr("AXME_AGENT_ADDRESS", "budget-envelope-validator") +) + +func envOr(k, d string) string { if v := os.Getenv(k); v != "" { return v }; return d } +func str(m map[string]any, k, d string) string { if v, ok := m[k].(string); ok { return v }; return d } + +var envelopes = map[string]float64{ + "cloud_infrastructure": 50000, "software_licenses": 20000, + "contractor_services": 40000, "hardware": 15000, +} + +func validateBudget(p map[string]any) (bool, string, map[string]any) { + budgetID := str(p, "budget_id", "?") + amount, _ := p["amount"].(float64) + currency := str(p, "currency", "USD") + category := str(p, "category", "") + + envelope, ok := envelopes[category] + if !ok { + keys := make([]string, 0, len(envelopes)) + for k := range envelopes { keys = append(keys, k) } + sort.Strings(keys) + return false, fmt.Sprintf("%s: unknown category %q; known: %v", budgetID, category, keys), nil + } + if amount > envelope { + return false, fmt.Sprintf("%s: %s %.0f exceeds quarterly envelope of %s %.0f for %q", budgetID, currency, amount, currency, envelope, category), + map[string]any{"envelope": envelope, "overage": amount - envelope} + } + headroom := math.Round((envelope-amount)/envelope*1000) / 10 + return true, fmt.Sprintf("%s: %s %.0f is within %q envelope (%.1f%% headroom)", budgetID, currency, amount, category, headroom), + map[string]any{"envelope": envelope, "headroom_pct": headroom} +} + +func main() { + if apiKey == "" { log.Fatal("AXME_API_KEY is required") } + client, err := axme.NewClient(axme.ClientConfig{APIKey: apiKey, BaseURL: baseURL}) + if err != nil { log.Fatalf("client init: %v", err) } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + log.Printf("budget-envelope-validator starting address=%s binding=stream", agentAddress) + + intents, errs := client.Listen(ctx, agentAddress, axme.ListenOptions{}) + for { + select { + case d, ok := <-intents: + if !ok { return } + id := str(d, "intent_id", ""); if id == "" { continue } + log.Printf("received intent %s", id) + + resp, _ := client.GetIntent(ctx, id, axme.RequestOptions{}) + intent, _ := resp["intent"].(map[string]any); if intent == nil { intent = resp } + status := strings.ToUpper(str(intent, "lifecycle_status", str(intent, "status", ""))) + if !map[string]bool{"CREATED": true, "DELIVERED": true, "ACKNOWLEDGED": true, "IN_PROGRESS": true, "WAITING": true}[status] { continue } + + raw, _ := intent["payload"].(map[string]any); if raw == nil { raw = map[string]any{} } + eff, _ := raw["parent_payload"].(map[string]any); if eff == nil { eff = raw } + + ok2, reason, meta := validateBudget(eff) + log.Printf("budget validation for %s: ok=%v", id, ok2) + + var payload map[string]any + if ok2 { + payload = map[string]any{"action": "complete", "within_envelope": true, "reason": reason} + if meta != nil { for k, v := range meta { payload[k] = v } } + } else { + payload = map[string]any{"action": "fail", "within_envelope": false, "reason": reason} + } + client.ResumeIntent(ctx, id, payload, axme.RequestOptions{OwnerAgent: agentAddress}) + log.Printf("resumed %s (within_envelope=%v)", id, ok2) + case err, ok := <-errs: if !ok { return }; log.Printf("error: %v", err) + case <-ctx.Done(): return + } + } +} diff --git a/examples/human/email/java/EmailAgent.java b/examples/human/email/java/EmailAgent.java new file mode 100644 index 0000000..2db9aca --- /dev/null +++ b/examples/human/email/java/EmailAgent.java @@ -0,0 +1,49 @@ +package ai.axme.examples.human; + +import ai.axme.examples.SseHelper; +import dev.axme.sdk.*; +import java.util.*; + +/** Budget Envelope Validator — human/email example (Java). */ +public class EmailAgent { + static final String BASE = SseHelper.env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + static final String KEY = System.getenv("AXME_API_KEY"); + static final String ADDR = SseHelper.env("AXME_AGENT_ADDRESS", "budget-envelope-validator"); + static final Map ENVELOPES = Map.of( + "cloud_infrastructure", 50000.0, "software_licenses", 20000.0, + "contractor_services", 40000.0, "hardware", 15000.0); + + public static void main(String[] args) throws Exception { + if (KEY == null || KEY.isEmpty()) { System.err.println("AXME_API_KEY is required"); System.exit(1); } + AxmeClient client = new AxmeClient(new AxmeClientConfig(BASE, KEY)); + System.out.printf("budget-envelope-validator starting address=%s%n", ADDR); + int since = 0; + while (true) { + try { + for (Map d : SseHelper.pollAgentStream(BASE, KEY, ADDR, since, 15)) { + String id = SseHelper.str(d, "intent_id"); Number seq = (Number) d.get("seq"); + if (seq != null) since = Math.max(since, seq.intValue()); if (id.isEmpty()) continue; + Map intent = SseHelper.unwrapIntent(client.getIntent(id, RequestOptions.none())); + if (!SseHelper.isActionable(intent)) continue; + Map p = SseHelper.effectivePayload(intent); + + double amount = p.get("amount") instanceof Number n ? n.doubleValue() : 0; + String category = SseHelper.str(p, "category"), currency = SseHelper.str(p, "currency", "USD"); + Double envelope = ENVELOPES.get(category); + + Map res = new HashMap<>(); + if (envelope == null) { res.put("action", "fail"); res.put("within_envelope", false); res.put("reason", "unknown category"); } + else if (amount > envelope) { res.put("action", "fail"); res.put("within_envelope", false); res.put("reason", "exceeds envelope"); } + else { + double headroom = Math.round((envelope - amount) / envelope * 1000.0) / 10.0; + res.put("action", "complete"); res.put("within_envelope", true); + res.put("reason", String.format("%s %.0f within %s envelope (%.1f%% headroom)", currency, amount, category, headroom)); + res.put("envelope", envelope); res.put("headroom_pct", headroom); + } + client.resumeIntent(id, res, SseHelper.withOwner(ADDR)); + System.out.printf("resumed %s (within_envelope=%s)%n", id, res.get("within_envelope")); + } + } catch (Exception e) { Thread.sleep(2000); } + } + } +} diff --git a/examples/human/email/agent.py b/examples/human/email/python/agent.py similarity index 100% rename from examples/human/email/agent.py rename to examples/human/email/python/agent.py diff --git a/examples/human/email/typescript/agent.ts b/examples/human/email/typescript/agent.ts new file mode 100644 index 0000000..18262e4 --- /dev/null +++ b/examples/human/email/typescript/agent.ts @@ -0,0 +1,85 @@ +/** + * Budget Envelope Validator — human/email example (TypeScript). + * + * Validates budget requests against quarterly allocations before + * routing to a human approver via email. + * + * Run: + * export AXME_API_KEY= + * export AXME_AGENT_ADDRESS=budget-envelope-validator + * npx tsx examples/human/email/agent.ts + */ +import { AxmeClient } from "@axme/axme"; + +const AXME_BASE_URL = process.env.AXME_BASE_URL ?? "https://api.cloud.axme.ai"; +const AXME_API_KEY = process.env.AXME_API_KEY ?? ""; +const AXME_AGENT_ADDRESS = process.env.AXME_AGENT_ADDRESS ?? "budget-envelope-validator"; + +if (!AXME_API_KEY) { console.error("AXME_API_KEY is required"); process.exit(1); } + +// --- Business logic --- +const QUARTERLY_ENVELOPES: Record = { + cloud_infrastructure: 50_000, software_licenses: 20_000, + contractor_services: 40_000, hardware: 15_000, +}; + +function validateBudget(payload: Record): { withinEnvelope: boolean; reason: string; meta: Record } { + const budgetId = String(payload.budget_id ?? "?"); + const amount = Number(payload.amount ?? 0); + const currency = String(payload.currency ?? "USD"); + const category = String(payload.category ?? ""); + + const envelope = QUARTERLY_ENVELOPES[category]; + if (envelope === undefined) { + return { withinEnvelope: false, reason: `${budgetId}: unknown category '${category}'; known: ${Object.keys(QUARTERLY_ENVELOPES).sort()}`, meta: {} }; + } + if (amount > envelope) { + return { withinEnvelope: false, reason: `${budgetId}: ${currency} ${amount.toLocaleString()} exceeds quarterly envelope of ${currency} ${envelope.toLocaleString()} for '${category}'`, meta: { envelope, overage: amount - envelope } }; + } + + const headroomPct = Math.round((envelope - amount) / envelope * 1000) / 10; + return { withinEnvelope: true, reason: `${budgetId}: ${currency} ${amount.toLocaleString()} is within '${category}' envelope (${headroomPct}% headroom)`, meta: { envelope, headroom_pct: headroomPct } }; +} + +// --- Agent loop --- +const client = new AxmeClient({ apiKey: AXME_API_KEY, baseUrl: AXME_BASE_URL }); +console.log(`budget-envelope-validator starting address=${AXME_AGENT_ADDRESS} binding=stream`); + +try { + for await (const delivery of client.listen(AXME_AGENT_ADDRESS)) { + const intentId = String(delivery.intent_id ?? ""); + if (!intentId) continue; + console.log(`received intent ${intentId}`); + + let intent: Record; + try { + const resp = await client.getIntent(intentId); + intent = (resp.intent ?? resp) as Record; + } catch (e) { console.error(`get_intent(${intentId}) failed:`, e); continue; } + + const status = String(intent.lifecycle_status ?? intent.status ?? "").toUpperCase(); + if (!["CREATED", "DELIVERED", "ACKNOWLEDGED", "IN_PROGRESS", "WAITING"].includes(status)) continue; + + const rawPayload = (intent.payload ?? {}) as Record; + const effectivePayload = (rawPayload.parent_payload ?? rawPayload) as Record; + const { withinEnvelope, reason, meta } = validateBudget(effectivePayload); + + console.log(`budget validation for ${intentId}: ok=${withinEnvelope} reason=${reason}`); + + try { + if (withinEnvelope) { + await client.resumeIntent(intentId, { + action: "complete", within_envelope: true, reason, + envelope: meta.envelope, headroom_pct: meta.headroom_pct, + }, { ownerAgent: AXME_AGENT_ADDRESS }); + } else { + await client.resumeIntent(intentId, { + action: "fail", within_envelope: false, reason, + }, { ownerAgent: AXME_AGENT_ADDRESS }); + } + console.log(`resumed intent ${intentId} (within_envelope=${withinEnvelope})`); + } catch (e) { console.error(`resume_intent(${intentId}) failed:`, e); } + } +} catch (e) { + if ((e as Error).name !== "AbortError") throw e; +} diff --git a/examples/human/form/dotnet/FormAgent.cs b/examples/human/form/dotnet/FormAgent.cs new file mode 100644 index 0000000..238fdb4 --- /dev/null +++ b/examples/human/form/dotnet/FormAgent.cs @@ -0,0 +1,57 @@ +using System.Text.Json.Nodes; +using Axme.Sdk; +using AxmeExamples; + +namespace AxmeExamples.Human; + +public class FormAgent +{ + public static async Task Main(string[] args) + { + var baseUrl = SseHelper.Env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + var apiKey = SseHelper.Env("AXME_API_KEY"); + var addr = SseHelper.Env("AXME_AGENT_ADDRESS", "access-policy-checker"); + if (string.IsNullOrEmpty(apiKey)) { Console.Error.WriteLine("AXME_API_KEY is required"); return; } + + var client = new AxmeClient(new AxmeClientConfig { ApiKey = apiKey, BaseUrl = baseUrl }); + Console.WriteLine($"access-policy-checker starting address={addr}"); + string[] allowed = ["READ", "READ_WRITE"]; string[] piiBlocked = ["READ_WRITE", "WRITE", "ADMIN"]; + string[] signoff = ["prod-analytics-db", "prod-customer-db", "prod-payments-db"]; + var since = 0; + + while (true) + { + try + { + foreach (var d in await SseHelper.PollAgentStreamAsync(baseUrl, apiKey, addr, since)) + { + var id = SseHelper.Str(d, "intent_id"); + if (d["seq"]?.GetValue() is int seq) since = Math.Max(since, seq); + if (string.IsNullOrEmpty(id)) continue; + var intent = SseHelper.UnwrapIntent(await client.GetIntentAsync(id)); + if (!SseHelper.IsActionable(intent)) continue; + var p = SseHelper.EffectivePayload(intent); + + var access = SseHelper.Str(p, "access_type").ToUpper(); + var requestor = SseHelper.Str(p, "requestor"); + var pii = p["pii_fields"]?.GetValue() == true; + var checks = new List(); var violations = new List(); + + if (allowed.Contains(access)) checks.Add("access ok"); else violations.Add("access not allowed"); + if (!string.IsNullOrEmpty(requestor)) checks.Add("requestor present"); else violations.Add("requestor missing"); + if (pii && piiBlocked.Contains(access)) violations.Add("PII write blocked"); + else if (pii) checks.Add("PII read ok"); else checks.Add("no PII"); + + var ok = violations.Count == 0; + var res = new JsonObject { ["action"] = ok ? "complete" : "fail", ["policy_passed"] = ok, + ["passed_checks"] = new JsonArray(checks.Select(s => (JsonNode)JsonValue.Create(s)!).ToArray()), + ["violations"] = new JsonArray(violations.Select(s => (JsonNode)JsonValue.Create(s)!).ToArray()) }; + if (ok) res["requires_human_signoff"] = signoff.Contains(SseHelper.Str(p, "resource")); + await client.ResumeIntentAsync(id, res, new RequestOptions { OwnerAgent = addr }); + Console.WriteLine($"resumed {id} (passed={ok})"); + } + } + catch { await Task.Delay(2000); } + } + } +} diff --git a/examples/human/form/go/agent.go b/examples/human/form/go/agent.go new file mode 100644 index 0000000..0e4193a --- /dev/null +++ b/examples/human/form/go/agent.go @@ -0,0 +1,96 @@ +// Access Policy Checker — human/form example (Go). +// +// Run: +// +// export AXME_API_KEY= +// export AXME_AGENT_ADDRESS=access-policy-checker +// go run examples/human/form/agent.go +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "strings" + + "github.com/AxmeAI/axme-sdk-go/axme" +) + +var ( + baseURL = envOr("AXME_BASE_URL", "https://api.cloud.axme.ai") + apiKey = os.Getenv("AXME_API_KEY") + agentAddress = envOr("AXME_AGENT_ADDRESS", "access-policy-checker") +) + +func envOr(k, d string) string { if v := os.Getenv(k); v != "" { return v }; return d } +func str(m map[string]any, k, d string) string { if v, ok := m[k].(string); ok { return v }; return d } + +var ( + allowedAccess = map[string]bool{"READ": true, "READ_WRITE": true} + humanSignoff = map[string]bool{"prod-analytics-db": true, "prod-customer-db": true, "prod-payments-db": true} + piiBlockedAccess = map[string]bool{"READ_WRITE": true, "WRITE": true, "ADMIN": true} +) + +func checkPolicy(p map[string]any) (bool, []string, []string) { + resource := str(p, "resource", "") + accessType := strings.ToUpper(str(p, "access_type", "")) + requestor := str(p, "requestor", "") + piiFields, _ := p["pii_fields"].(bool) + var checks, violations []string + + if allowedAccess[accessType] { checks = append(checks, fmt.Sprintf("access type '%s' is in allowed set", accessType)) } else { violations = append(violations, fmt.Sprintf("access type '%s' is not allowed", accessType)) } + if requestor != "" { checks = append(checks, fmt.Sprintf("requestor identity '%s' is present", requestor)) } else { violations = append(violations, "requestor identity is missing") } + if piiFields && piiBlockedAccess[accessType] { + violations = append(violations, fmt.Sprintf("resource contains PII fields; '%s' access is not permitted on PII data", accessType)) + } else if piiFields { + checks = append(checks, fmt.Sprintf("PII data present but '%s' is read-only — acceptable", accessType)) + } else { + checks = append(checks, "resource has no PII fields — no PII restriction applies") + } + _ = resource + return len(violations) == 0, checks, violations +} + +func main() { + if apiKey == "" { log.Fatal("AXME_API_KEY is required") } + client, err := axme.NewClient(axme.ClientConfig{APIKey: apiKey, BaseURL: baseURL}) + if err != nil { log.Fatalf("client init: %v", err) } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + log.Printf("access-policy-checker starting address=%s binding=stream", agentAddress) + + intents, errs := client.Listen(ctx, agentAddress, axme.ListenOptions{}) + for { + select { + case d, ok := <-intents: + if !ok { return } + id := str(d, "intent_id", ""); if id == "" { continue } + + resp, _ := client.GetIntent(ctx, id, axme.RequestOptions{}) + intent, _ := resp["intent"].(map[string]any); if intent == nil { intent = resp } + status := strings.ToUpper(str(intent, "lifecycle_status", str(intent, "status", ""))) + if !map[string]bool{"CREATED": true, "DELIVERED": true, "ACKNOWLEDGED": true, "IN_PROGRESS": true, "WAITING": true}[status] { continue } + + raw, _ := intent["payload"].(map[string]any); if raw == nil { raw = map[string]any{} } + eff, _ := raw["parent_payload"].(map[string]any); if eff == nil { eff = raw } + + passed, checks, violations := checkPolicy(eff) + log.Printf("policy check for %s: passed=%v", id, passed) + + var payload map[string]any + if passed { + payload = map[string]any{"action": "complete", "policy_passed": true, "passed_checks": checks, "violations": []string{}, + "requires_human_signoff": humanSignoff[str(eff, "resource", "")]} + } else { + payload = map[string]any{"action": "fail", "policy_passed": false, "passed_checks": checks, "violations": violations} + } + client.ResumeIntent(ctx, id, payload, axme.RequestOptions{OwnerAgent: agentAddress}) + log.Printf("resumed %s (passed=%v)", id, passed) + case err, ok := <-errs: if !ok { return }; log.Printf("error: %v", err) + case <-ctx.Done(): return + } + } +} diff --git a/examples/human/form/java/FormAgent.java b/examples/human/form/java/FormAgent.java new file mode 100644 index 0000000..9f22229 --- /dev/null +++ b/examples/human/form/java/FormAgent.java @@ -0,0 +1,50 @@ +package ai.axme.examples.human; + +import ai.axme.examples.SseHelper; +import dev.axme.sdk.*; +import java.util.*; + +/** Access Policy Checker — human/form example (Java). */ +public class FormAgent { + static final String BASE = SseHelper.env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + static final String KEY = System.getenv("AXME_API_KEY"); + static final String ADDR = SseHelper.env("AXME_AGENT_ADDRESS", "access-policy-checker"); + static final Set ALLOWED_ACCESS = Set.of("READ", "READ_WRITE"); + static final Set PII_BLOCKED = Set.of("READ_WRITE", "WRITE", "ADMIN"); + static final Set SIGNOFF_RES = Set.of("prod-analytics-db", "prod-customer-db", "prod-payments-db"); + + public static void main(String[] args) throws Exception { + if (KEY == null || KEY.isEmpty()) { System.err.println("AXME_API_KEY is required"); System.exit(1); } + AxmeClient client = new AxmeClient(new AxmeClientConfig(BASE, KEY)); + System.out.printf("access-policy-checker starting address=%s%n", ADDR); + int since = 0; + while (true) { + try { + for (Map d : SseHelper.pollAgentStream(BASE, KEY, ADDR, since, 15)) { + String id = SseHelper.str(d, "intent_id"); Number seq = (Number) d.get("seq"); + if (seq != null) since = Math.max(since, seq.intValue()); if (id.isEmpty()) continue; + Map intent = SseHelper.unwrapIntent(client.getIntent(id, RequestOptions.none())); + if (!SseHelper.isActionable(intent)) continue; + Map p = SseHelper.effectivePayload(intent); + + String access = SseHelper.str(p, "access_type").toUpperCase(), requestor = SseHelper.str(p, "requestor"); + boolean pii = Boolean.TRUE.equals(p.get("pii_fields")); + List checks = new ArrayList<>(), violations = new ArrayList<>(); + + if (ALLOWED_ACCESS.contains(access)) checks.add("access type ok"); else violations.add("access type not allowed"); + if (!requestor.isEmpty()) checks.add("requestor present"); else violations.add("requestor missing"); + if (pii && PII_BLOCKED.contains(access)) violations.add("PII write blocked"); + else if (pii) checks.add("PII read-only ok"); else checks.add("no PII"); + + boolean ok = violations.isEmpty(); + Map res = new HashMap<>(); + res.put("action", ok ? "complete" : "fail"); res.put("policy_passed", ok); + res.put("passed_checks", checks); res.put("violations", violations); + if (ok) res.put("requires_human_signoff", SIGNOFF_RES.contains(SseHelper.str(p, "resource"))); + client.resumeIntent(id, res, SseHelper.withOwner(ADDR)); + System.out.printf("resumed %s (passed=%s)%n", id, ok); + } + } catch (Exception e) { Thread.sleep(2000); } + } + } +} diff --git a/examples/human/form/agent.py b/examples/human/form/python/agent.py similarity index 100% rename from examples/human/form/agent.py rename to examples/human/form/python/agent.py diff --git a/examples/human/form/typescript/agent.ts b/examples/human/form/typescript/agent.ts new file mode 100644 index 0000000..0a0ac8b --- /dev/null +++ b/examples/human/form/typescript/agent.ts @@ -0,0 +1,91 @@ +/** + * Access Policy Checker — human/form example (TypeScript). + * + * Checks access request policy before routing to the Security Officer + * for structured form-based approval. + * + * Run: + * export AXME_API_KEY= + * export AXME_AGENT_ADDRESS=access-policy-checker + * npx tsx examples/human/form/agent.ts + */ +import { AxmeClient } from "@axme/axme"; + +const AXME_BASE_URL = process.env.AXME_BASE_URL ?? "https://api.cloud.axme.ai"; +const AXME_API_KEY = process.env.AXME_API_KEY ?? ""; +const AXME_AGENT_ADDRESS = process.env.AXME_AGENT_ADDRESS ?? "access-policy-checker"; + +if (!AXME_API_KEY) { console.error("AXME_API_KEY is required"); process.exit(1); } + +// --- Business logic --- +const ALLOWED_ACCESS_TYPES = new Set(["READ", "READ_WRITE"]); +const HUMAN_SIGNOFF_RESOURCES = new Set(["prod-analytics-db", "prod-customer-db", "prod-payments-db"]); +const PII_BLOCKED_ACCESS = new Set(["READ_WRITE", "WRITE", "ADMIN"]); + +function checkPolicy(payload: Record): { passed: boolean; checks: string[]; violations: string[] } { + const resource = String(payload.resource ?? ""); + const accessType = String(payload.access_type ?? "").toUpperCase(); + const requestor = String(payload.requestor ?? ""); + const piiFields = Boolean(payload.pii_fields); + const checks: string[] = []; + const violations: string[] = []; + + if (ALLOWED_ACCESS_TYPES.has(accessType)) checks.push(`access type '${accessType}' is in allowed set`); + else violations.push(`access type '${accessType}' is not allowed; permitted: ${[...ALLOWED_ACCESS_TYPES].sort()}`); + + if (requestor) checks.push(`requestor identity '${requestor}' is present`); + else violations.push("requestor identity is missing"); + + if (piiFields && PII_BLOCKED_ACCESS.has(accessType)) { + violations.push(`resource contains PII fields; '${accessType}' access is not permitted on PII data`); + } else if (piiFields) { + checks.push(`PII data present but '${accessType}' is read-only — acceptable`); + } else { + checks.push("resource has no PII fields — no PII restriction applies"); + } + + return { passed: violations.length === 0, checks, violations }; +} + +// --- Agent loop --- +const client = new AxmeClient({ apiKey: AXME_API_KEY, baseUrl: AXME_BASE_URL }); +console.log(`access-policy-checker starting address=${AXME_AGENT_ADDRESS} binding=stream`); + +try { + for await (const delivery of client.listen(AXME_AGENT_ADDRESS)) { + const intentId = String(delivery.intent_id ?? ""); + if (!intentId) continue; + console.log(`received intent ${intentId}`); + + let intent: Record; + try { + const resp = await client.getIntent(intentId); + intent = (resp.intent ?? resp) as Record; + } catch (e) { console.error(`get_intent(${intentId}) failed:`, e); continue; } + + const status = String(intent.lifecycle_status ?? intent.status ?? "").toUpperCase(); + if (!["CREATED", "DELIVERED", "ACKNOWLEDGED", "IN_PROGRESS", "WAITING"].includes(status)) continue; + + const rawPayload = (intent.payload ?? {}) as Record; + const effectivePayload = (rawPayload.parent_payload ?? rawPayload) as Record; + const { passed, checks, violations } = checkPolicy(effectivePayload); + + console.log(`policy check for ${intentId}: passed=${passed} checks=${checks.length} violations=${violations.length}`); + + try { + if (passed) { + await client.resumeIntent(intentId, { + action: "complete", policy_passed: true, passed_checks: checks, violations: [], + requires_human_signoff: HUMAN_SIGNOFF_RESOURCES.has(String(effectivePayload.resource ?? "")), + }, { ownerAgent: AXME_AGENT_ADDRESS }); + } else { + await client.resumeIntent(intentId, { + action: "fail", policy_passed: false, passed_checks: checks, violations, + }, { ownerAgent: AXME_AGENT_ADDRESS }); + } + console.log(`resumed intent ${intentId} (policy_passed=${passed})`); + } catch (e) { console.error(`resume_intent(${intentId}) failed:`, e); } + } +} catch (e) { + if ((e as Error).name !== "AbortError") throw e; +} diff --git a/examples/internal/delay/dotnet/DelayAgent.cs b/examples/internal/delay/dotnet/DelayAgent.cs new file mode 100644 index 0000000..4be6e4a --- /dev/null +++ b/examples/internal/delay/dotnet/DelayAgent.cs @@ -0,0 +1,52 @@ +using System.Text.Json.Nodes; +using Axme.Sdk; +using AxmeExamples; + +namespace AxmeExamples.Internal; + +public class DelayAgent +{ + public static async Task Main(string[] args) + { + var baseUrl = SseHelper.Env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + var apiKey = SseHelper.Env("AXME_API_KEY"); + var addr = SseHelper.Env("AXME_AGENT_ADDRESS", "change-window-validator"); + if (string.IsNullOrEmpty(apiKey)) { Console.Error.WriteLine("AXME_API_KEY is required"); return; } + + var client = new AxmeClient(new AxmeClientConfig { ApiKey = apiKey, BaseUrl = baseUrl }); + Console.WriteLine($"change-window-validator starting address={addr}"); + string[] windows = ["MW-2026-Q1-01","MW-2026-Q1-02","MW-2026-Q1-03","MW-2026-Q1-04","MW-2026-Q1-05","MW-2026-Q1-06"]; + string[] types = ["config_update","dependency_update","rollback","hotfix"]; + var since = 0; + + while (true) + { + try + { + foreach (var d in await SseHelper.PollAgentStreamAsync(baseUrl, apiKey, addr, since)) + { + var id = SseHelper.Str(d, "intent_id"); + if (d["seq"]?.GetValue() is int seq) since = Math.Max(since, seq); + if (string.IsNullOrEmpty(id)) continue; + var intent = SseHelper.UnwrapIntent(await client.GetIntentAsync(id)); + if (!SseHelper.IsActionable(intent)) continue; + var p = SseHelper.EffectivePayload(intent); + + var cid = SseHelper.Str(p, "change_id", "?"); var env = SseHelper.Str(p, "environment"); + var ct = SseHelper.Str(p, "change_type"); var wid = SseHelper.Str(p, "window_id"); + bool ok = true; string reason = $"{cid}: validated"; + + if (!types.Contains(ct)) { ok = false; reason = $"{cid}: change_type not allowed"; } + else if (env == "production" && string.IsNullOrEmpty(wid)) { ok = false; reason = $"{cid}: production requires window_id"; } + else if (env == "production" && !windows.Contains(wid)) { ok = false; reason = $"{cid}: window_id not approved"; } + + var res = new JsonObject { ["action"] = ok ? "complete" : "fail", ["allowed"] = ok, ["reason"] = reason }; + if (ok && !string.IsNullOrEmpty(wid) && windows.Contains(wid)) { res["window_id"] = wid; res["window_approved"] = true; } + await client.ResumeIntentAsync(id, res, new RequestOptions { OwnerAgent = addr }); + Console.WriteLine($"resumed {id} (allowed={ok})"); + } + } + catch { await Task.Delay(2000); } + } + } +} diff --git a/examples/internal/delay/go/agent.go b/examples/internal/delay/go/agent.go new file mode 100644 index 0000000..f09b14f --- /dev/null +++ b/examples/internal/delay/go/agent.go @@ -0,0 +1,91 @@ +// Change Window Validator — internal/delay example (Go). +// +// Run: +// +// export AXME_API_KEY= +// export AXME_AGENT_ADDRESS=change-window-validator +// go run examples/internal/delay/agent.go +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "strings" + "time" + + "github.com/AxmeAI/axme-sdk-go/axme" +) + +var ( + baseURL = envOr("AXME_BASE_URL", "https://api.cloud.axme.ai") + apiKey = os.Getenv("AXME_API_KEY") + agentAddress = envOr("AXME_AGENT_ADDRESS", "change-window-validator") +) + +func envOr(k, d string) string { if v := os.Getenv(k); v != "" { return v }; return d } +func str(m map[string]any, k, d string) string { if v, ok := m[k].(string); ok { return v }; return d } + +var ( + approvedWindows = map[string]bool{"MW-2026-Q1-01": true, "MW-2026-Q1-02": true, "MW-2026-Q1-03": true, "MW-2026-Q1-04": true, "MW-2026-Q1-05": true, "MW-2026-Q1-06": true} + allowedTypes = map[string]bool{"config_update": true, "dependency_update": true, "rollback": true, "hotfix": true} +) + +func validateWindow(p map[string]any) (bool, string, map[string]any) { + changeID := str(p, "change_id", "?") + env := str(p, "environment", "") + changeType := str(p, "change_type", "") + windowID := str(p, "window_id", "") + scheduledAt := str(p, "scheduled_at", "") + + if !allowedTypes[changeType] { return false, fmt.Sprintf("%s: change_type '%s' is not allowed", changeID, changeType), nil } + if env == "production" { + if windowID == "" { return false, fmt.Sprintf("%s: production changes require an approved window_id", changeID), nil } + if !approvedWindows[windowID] { return false, fmt.Sprintf("%s: window_id '%s' is not approved", changeID, windowID), nil } + } + if scheduledAt != "" { + if _, err := time.Parse(time.RFC3339, scheduledAt); err != nil { + return false, fmt.Sprintf("%s: scheduled_at '%s' is not a valid ISO timestamp", changeID, scheduledAt), nil + } + } + meta := map[string]any{} + if windowID != "" && approvedWindows[windowID] { meta["window_id"] = windowID; meta["window_approved"] = true } + return true, fmt.Sprintf("%s: change window validated for '%s' environment", changeID, env), meta +} + +func main() { + if apiKey == "" { log.Fatal("AXME_API_KEY is required") } + client, err := axme.NewClient(axme.ClientConfig{APIKey: apiKey, BaseURL: baseURL}) + if err != nil { log.Fatalf("client init: %v", err) } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + log.Printf("change-window-validator starting address=%s binding=stream", agentAddress) + + intents, errs := client.Listen(ctx, agentAddress, axme.ListenOptions{}) + for { + select { + case d, ok := <-intents: + if !ok { return } + id := str(d, "intent_id", ""); if id == "" { continue } + resp, _ := client.GetIntent(ctx, id, axme.RequestOptions{}) + intent, _ := resp["intent"].(map[string]any); if intent == nil { intent = resp } + status := strings.ToUpper(str(intent, "lifecycle_status", str(intent, "status", ""))) + if !map[string]bool{"CREATED": true, "DELIVERED": true, "ACKNOWLEDGED": true, "IN_PROGRESS": true, "WAITING": true}[status] { continue } + raw, _ := intent["payload"].(map[string]any); if raw == nil { raw = map[string]any{} } + eff, _ := raw["parent_payload"].(map[string]any); if eff == nil { eff = raw } + + allowed, reason, meta := validateWindow(eff) + log.Printf("window validation for %s: allowed=%v", id, allowed) + payload := map[string]any{"action": "complete", "allowed": true, "reason": reason} + if !allowed { payload = map[string]any{"action": "fail", "allowed": false, "reason": reason} } + if meta != nil { for k, v := range meta { payload[k] = v } } + client.ResumeIntent(ctx, id, payload, axme.RequestOptions{OwnerAgent: agentAddress}) + log.Printf("resumed %s (allowed=%v)", id, allowed) + case err, ok := <-errs: if !ok { return }; log.Printf("error: %v", err) + case <-ctx.Done(): return + } + } +} diff --git a/examples/internal/delay/java/DelayAgent.java b/examples/internal/delay/java/DelayAgent.java new file mode 100644 index 0000000..1c9f0e6 --- /dev/null +++ b/examples/internal/delay/java/DelayAgent.java @@ -0,0 +1,45 @@ +package ai.axme.examples.internal; + +import ai.axme.examples.SseHelper; +import dev.axme.sdk.*; +import java.util.*; + +/** Change Window Validator — internal/delay example (Java). */ +public class DelayAgent { + static final String BASE = SseHelper.env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + static final String KEY = System.getenv("AXME_API_KEY"); + static final String ADDR = SseHelper.env("AXME_AGENT_ADDRESS", "change-window-validator"); + static final Set WINDOWS = Set.of("MW-2026-Q1-01","MW-2026-Q1-02","MW-2026-Q1-03","MW-2026-Q1-04","MW-2026-Q1-05","MW-2026-Q1-06"); + static final Set TYPES = Set.of("config_update","dependency_update","rollback","hotfix"); + + public static void main(String[] args) throws Exception { + if (KEY == null || KEY.isEmpty()) { System.err.println("AXME_API_KEY is required"); System.exit(1); } + AxmeClient client = new AxmeClient(new AxmeClientConfig(BASE, KEY)); + System.out.printf("change-window-validator starting address=%s%n", ADDR); + int since = 0; + while (true) { + try { + for (Map d : SseHelper.pollAgentStream(BASE, KEY, ADDR, since, 15)) { + String id = SseHelper.str(d, "intent_id"); Number seq = (Number) d.get("seq"); + if (seq != null) since = Math.max(since, seq.intValue()); if (id.isEmpty()) continue; + Map intent = SseHelper.unwrapIntent(client.getIntent(id, RequestOptions.none())); + if (!SseHelper.isActionable(intent)) continue; + Map p = SseHelper.effectivePayload(intent); + + String changeId = SseHelper.str(p, "change_id", "?"), env = SseHelper.str(p, "environment"); + String type = SseHelper.str(p, "change_type"), windowId = SseHelper.str(p, "window_id"); + + Map res = new HashMap<>(); + boolean ok = true; String reason = changeId + ": validated"; + if (!TYPES.contains(type)) { ok = false; reason = changeId + ": change_type not allowed"; } + else if ("production".equals(env) && windowId.isEmpty()) { ok = false; reason = changeId + ": production requires window_id"; } + else if ("production".equals(env) && !WINDOWS.contains(windowId)) { ok = false; reason = changeId + ": window_id not approved"; } + if (ok && !windowId.isEmpty() && WINDOWS.contains(windowId)) { res.put("window_id", windowId); res.put("window_approved", true); } + res.put("action", ok ? "complete" : "fail"); res.put("allowed", ok); res.put("reason", reason); + client.resumeIntent(id, res, SseHelper.withOwner(ADDR)); + System.out.printf("resumed %s (allowed=%s)%n", id, ok); + } + } catch (Exception e) { Thread.sleep(2000); } + } + } +} diff --git a/examples/internal/delay/agent.py b/examples/internal/delay/python/agent.py similarity index 100% rename from examples/internal/delay/agent.py rename to examples/internal/delay/python/agent.py diff --git a/examples/internal/delay/typescript/agent.ts b/examples/internal/delay/typescript/agent.ts new file mode 100644 index 0000000..982302e --- /dev/null +++ b/examples/internal/delay/typescript/agent.ts @@ -0,0 +1,85 @@ +/** + * Change Window Validator — internal/delay example (TypeScript). + * + * Validates that a change falls within an approved maintenance window. + * The step has a 120-second deadline (step_deadline_seconds: 120). + * + * Run: + * export AXME_API_KEY= + * export AXME_AGENT_ADDRESS=change-window-validator + * npx tsx examples/internal/delay/agent.ts + */ +import { AxmeClient } from "@axme/axme"; + +const AXME_BASE_URL = process.env.AXME_BASE_URL ?? "https://api.cloud.axme.ai"; +const AXME_API_KEY = process.env.AXME_API_KEY ?? ""; +const AXME_AGENT_ADDRESS = process.env.AXME_AGENT_ADDRESS ?? "change-window-validator"; + +if (!AXME_API_KEY) { console.error("AXME_API_KEY is required"); process.exit(1); } + +// --- Business logic --- +const APPROVED_WINDOWS = new Set(["MW-2026-Q1-01", "MW-2026-Q1-02", "MW-2026-Q1-03", "MW-2026-Q1-04", "MW-2026-Q1-05", "MW-2026-Q1-06"]); +const ALLOWED_CHANGE_TYPES = new Set(["config_update", "dependency_update", "rollback", "hotfix"]); + +function validateChangeWindow(payload: Record): { allowed: boolean; reason: string; meta: Record } { + const changeId = String(payload.change_id ?? "?"); + const environment = String(payload.environment ?? ""); + const changeType = String(payload.change_type ?? ""); + const windowId = String(payload.window_id ?? ""); + const scheduledAt = String(payload.scheduled_at ?? ""); + + if (!ALLOWED_CHANGE_TYPES.has(changeType)) { + return { allowed: false, reason: `${changeId}: change_type '${changeType}' is not in allowed set ${[...ALLOWED_CHANGE_TYPES].sort()}`, meta: {} }; + } + if (environment === "production") { + if (!windowId) return { allowed: false, reason: `${changeId}: production changes require an approved window_id`, meta: {} }; + if (!APPROVED_WINDOWS.has(windowId)) return { allowed: false, reason: `${changeId}: window_id '${windowId}' is not in the approved window list`, meta: {} }; + } + if (scheduledAt) { + const d = new Date(scheduledAt); + if (isNaN(d.getTime())) return { allowed: false, reason: `${changeId}: scheduled_at '${scheduledAt}' is not a valid ISO timestamp`, meta: {} }; + } + + const meta: Record = {}; + if (windowId && APPROVED_WINDOWS.has(windowId)) Object.assign(meta, { window_id: windowId, window_approved: true }); + + return { allowed: true, reason: `${changeId}: change window validated for '${environment}' environment`, meta }; +} + +// --- Agent loop --- +const client = new AxmeClient({ apiKey: AXME_API_KEY, baseUrl: AXME_BASE_URL }); +console.log(`change-window-validator starting address=${AXME_AGENT_ADDRESS} binding=stream`); + +try { + for await (const delivery of client.listen(AXME_AGENT_ADDRESS)) { + const intentId = String(delivery.intent_id ?? ""); + if (!intentId) continue; + console.log(`received intent ${intentId}`); + + let intent: Record; + try { + const resp = await client.getIntent(intentId); + intent = (resp.intent ?? resp) as Record; + } catch (e) { console.error(`get_intent(${intentId}) failed:`, e); continue; } + + const status = String(intent.lifecycle_status ?? intent.status ?? "").toUpperCase(); + if (!["CREATED", "DELIVERED", "ACKNOWLEDGED", "IN_PROGRESS", "WAITING"].includes(status)) continue; + + const rawPayload = (intent.payload ?? {}) as Record; + const effectivePayload = (rawPayload.parent_payload ?? rawPayload) as Record; + const { allowed, reason, meta } = validateChangeWindow(effectivePayload); + + console.log(`window validation for ${intentId}: allowed=${allowed} reason=${reason}`); + + try { + if (allowed) { + await client.resumeIntent(intentId, { action: "complete", allowed: true, reason, ...meta }, { ownerAgent: AXME_AGENT_ADDRESS }); + } else { + await client.resumeIntent(intentId, { action: "fail", allowed: false, reason }, { ownerAgent: AXME_AGENT_ADDRESS }); + } + console.log(`resumed intent ${intentId} (allowed=${allowed})`); + } catch (e) { console.error(`resume_intent(${intentId}) failed:`, e); } + } +} catch (e) { + if ((e as Error).name !== "AbortError") throw e; +} diff --git a/examples/internal/escalation/dotnet/EscalationAgent.cs b/examples/internal/escalation/dotnet/EscalationAgent.cs new file mode 100644 index 0000000..53cda9f --- /dev/null +++ b/examples/internal/escalation/dotnet/EscalationAgent.cs @@ -0,0 +1,52 @@ +using System.Text.Json.Nodes; +using Axme.Sdk; +using AxmeExamples; + +namespace AxmeExamples.Internal; + +public class EscalationAgent +{ + public static async Task Main(string[] args) + { + var baseUrl = SseHelper.Env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + var apiKey = SseHelper.Env("AXME_API_KEY"); + var addr = SseHelper.Env("AXME_AGENT_ADDRESS", "incident-classifier-agent"); + if (string.IsNullOrEmpty(apiKey)) { Console.Error.WriteLine("AXME_API_KEY is required"); return; } + + var client = new AxmeClient(new AxmeClientConfig { ApiKey = apiKey, BaseUrl = baseUrl }); + Console.WriteLine($"incident-classifier starting address={addr}"); + var sla = new Dictionary { ["P1"] = 5, ["P2"] = 15, ["P3"] = 60, ["P4"] = 240 }; + string[] critical = ["api-gateway", "auth-service", "payments-processor"]; + var since = 0; + + while (true) + { + try + { + foreach (var d in await SseHelper.PollAgentStreamAsync(baseUrl, apiKey, addr, since)) + { + var id = SseHelper.Str(d, "intent_id"); + if (d["seq"]?.GetValue() is int seq) since = Math.Max(since, seq); + if (string.IsNullOrEmpty(id)) continue; + var intent = SseHelper.UnwrapIntent(await client.GetIntentAsync(id)); + if (!SseHelper.IsActionable(intent)) continue; + var p = SseHelper.EffectivePayload(intent); + + var sev = SseHelper.Str(p, "severity", "P3"); var svc = SseHelper.Str(p, "service"); + var symptom = SseHelper.Str(p, "symptom"); + var isCrit = critical.Contains(svc); var minutes = sla.GetValueOrDefault(sev, 60); + var affected = new List(); if (!string.IsNullOrEmpty(svc)) affected.Add(svc); if (isCrit) affected.Add("downstream-dependents"); + var esc = (sev is "P1" or "P2") && isCrit ? "on-call → team-lead → engineering-director" : sev is "P1" or "P2" ? "on-call → team-lead" : "on-call"; + + var res = new JsonObject { ["action"] = "complete", ["incident_id"] = SseHelper.Str(p, "incident_id", "?"), + ["severity"] = sev, ["is_critical_service"] = isCrit, ["sla_minutes"] = minutes, + ["affected_components"] = new JsonArray(affected.Select(a => (JsonNode)JsonValue.Create(a)!).ToArray()), + ["escalation_path"] = esc, ["has_error_rate_signal"] = symptom.Contains("error rate", StringComparison.OrdinalIgnoreCase) || symptom.Contains("5xx") }; + await client.ResumeIntentAsync(id, res, new RequestOptions { OwnerAgent = addr }); + Console.WriteLine($"resumed {id}"); + } + } + catch { await Task.Delay(2000); } + } + } +} diff --git a/examples/internal/escalation/go/agent.go b/examples/internal/escalation/go/agent.go new file mode 100644 index 0000000..f2aa3fe --- /dev/null +++ b/examples/internal/escalation/go/agent.go @@ -0,0 +1,89 @@ +// Incident Classifier Agent — internal/escalation example (Go). +// +// Run: +// +// export AXME_API_KEY= +// export AXME_AGENT_ADDRESS=incident-classifier-agent +// go run examples/internal/escalation/agent.go +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "strings" + + "github.com/AxmeAI/axme-sdk-go/axme" +) + +var ( + baseURL = envOr("AXME_BASE_URL", "https://api.cloud.axme.ai") + apiKey = os.Getenv("AXME_API_KEY") + agentAddress = envOr("AXME_AGENT_ADDRESS", "incident-classifier-agent") +) + +func envOr(k, d string) string { if v := os.Getenv(k); v != "" { return v }; return d } +func str(m map[string]any, k, d string) string { if v, ok := m[k].(string); ok { return v }; return d } + +var severitySLA = map[string]int{"P1": 5, "P2": 15, "P3": 60, "P4": 240} +var criticalServices = map[string]bool{"api-gateway": true, "auth-service": true, "payments-processor": true} + +func classifyIncident(p map[string]any) map[string]any { + incidentID := str(p, "incident_id", "?") + severity := str(p, "severity", "P3") + service := str(p, "service", "") + symptom := str(p, "symptom", "") + sla := severitySLA[severity]; if sla == 0 { sla = 60 } + isCritical := criticalServices[service] + + affected := []string{} + if service != "" { affected = append(affected, service) } + if isCritical { affected = append(affected, "downstream-dependents") } + + escalation := "on-call" + if (severity == "P1" || severity == "P2") && isCritical { escalation = "on-call → team-lead → engineering-director" } else if severity == "P1" || severity == "P2" { escalation = "on-call → team-lead" } + + hasErrorRate := strings.Contains(strings.ToLower(symptom), "error rate") || strings.Contains(strings.ToLower(symptom), "5xx") + + return map[string]any{ + "incident_id": incidentID, "severity": severity, "is_critical_service": isCritical, + "has_error_rate_signal": hasErrorRate, "affected_components": affected, + "sla_minutes": sla, "escalation_path": escalation, + "classification_note": fmt.Sprintf("%s: %s on '%s' — SLA=%dm, escalation: %s", incidentID, severity, service, sla, escalation), + } +} + +func main() { + if apiKey == "" { log.Fatal("AXME_API_KEY is required") } + client, err := axme.NewClient(axme.ClientConfig{APIKey: apiKey, BaseURL: baseURL}) + if err != nil { log.Fatalf("client init: %v", err) } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + log.Printf("incident-classifier starting address=%s binding=stream", agentAddress) + + intents, errs := client.Listen(ctx, agentAddress, axme.ListenOptions{}) + for { + select { + case d, ok := <-intents: + if !ok { return } + id := str(d, "intent_id", ""); if id == "" { continue } + resp, _ := client.GetIntent(ctx, id, axme.RequestOptions{}) + intent, _ := resp["intent"].(map[string]any); if intent == nil { intent = resp } + status := strings.ToUpper(str(intent, "lifecycle_status", str(intent, "status", ""))) + if !map[string]bool{"CREATED": true, "DELIVERED": true, "ACKNOWLEDGED": true, "IN_PROGRESS": true, "WAITING": true}[status] { continue } + raw, _ := intent["payload"].(map[string]any); if raw == nil { raw = map[string]any{} } + eff, _ := raw["parent_payload"].(map[string]any); if eff == nil { eff = raw } + + result := classifyIncident(eff) + log.Printf("classified %s: %s", id, result["classification_note"]) + result["action"] = "complete" + client.ResumeIntent(ctx, id, result, axme.RequestOptions{OwnerAgent: agentAddress}) + log.Printf("resumed %s", id) + case err, ok := <-errs: if !ok { return }; log.Printf("error: %v", err) + case <-ctx.Done(): return + } + } +} diff --git a/examples/internal/escalation/java/EscalationAgent.java b/examples/internal/escalation/java/EscalationAgent.java new file mode 100644 index 0000000..babd569 --- /dev/null +++ b/examples/internal/escalation/java/EscalationAgent.java @@ -0,0 +1,47 @@ +package ai.axme.examples.internal; + +import ai.axme.examples.SseHelper; +import dev.axme.sdk.*; +import java.util.*; + +/** Incident Classifier Agent — internal/escalation example (Java). */ +public class EscalationAgent { + static final String BASE = SseHelper.env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + static final String KEY = System.getenv("AXME_API_KEY"); + static final String ADDR = SseHelper.env("AXME_AGENT_ADDRESS", "incident-classifier-agent"); + static final Map SLA = Map.of("P1", 5, "P2", 15, "P3", 60, "P4", 240); + static final Set CRITICAL = Set.of("api-gateway", "auth-service", "payments-processor"); + + public static void main(String[] args) throws Exception { + if (KEY == null || KEY.isEmpty()) { System.err.println("AXME_API_KEY is required"); System.exit(1); } + AxmeClient client = new AxmeClient(new AxmeClientConfig(BASE, KEY)); + System.out.printf("incident-classifier starting address=%s%n", ADDR); + int since = 0; + while (true) { + try { + for (Map d : SseHelper.pollAgentStream(BASE, KEY, ADDR, since, 15)) { + String id = SseHelper.str(d, "intent_id"); Number seq = (Number) d.get("seq"); + if (seq != null) since = Math.max(since, seq.intValue()); if (id.isEmpty()) continue; + Map intent = SseHelper.unwrapIntent(client.getIntent(id, RequestOptions.none())); + if (!SseHelper.isActionable(intent)) continue; + Map p = SseHelper.effectivePayload(intent); + + String incId = SseHelper.str(p, "incident_id", "?"), sev = SseHelper.str(p, "severity", "P3"); + String svc = SseHelper.str(p, "service"), symptom = SseHelper.str(p, "symptom"); + int sla = SLA.getOrDefault(sev, 60); boolean crit = CRITICAL.contains(svc); + List affected = new ArrayList<>(); if (!svc.isEmpty()) affected.add(svc); if (crit) affected.add("downstream-dependents"); + String esc = ("P1".equals(sev) || "P2".equals(sev)) && crit ? "on-call → team-lead → engineering-director" : + ("P1".equals(sev) || "P2".equals(sev)) ? "on-call → team-lead" : "on-call"; + + Map res = new HashMap<>(); + res.put("action", "complete"); res.put("incident_id", incId); res.put("severity", sev); + res.put("is_critical_service", crit); res.put("affected_components", affected); + res.put("sla_minutes", sla); res.put("escalation_path", esc); + res.put("has_error_rate_signal", symptom.toLowerCase().contains("error rate") || symptom.contains("5xx")); + client.resumeIntent(id, res, SseHelper.withOwner(ADDR)); + System.out.printf("resumed %s%n", id); + } + } catch (Exception e) { Thread.sleep(2000); } + } + } +} diff --git a/examples/internal/escalation/agent.py b/examples/internal/escalation/python/agent.py similarity index 100% rename from examples/internal/escalation/agent.py rename to examples/internal/escalation/python/agent.py diff --git a/examples/internal/escalation/typescript/agent.ts b/examples/internal/escalation/typescript/agent.ts new file mode 100644 index 0000000..59ede8c --- /dev/null +++ b/examples/internal/escalation/typescript/agent.ts @@ -0,0 +1,81 @@ +/** + * Incident Classifier Agent — internal/escalation example (TypeScript). + * + * Classifies incidents by severity and affected components. + * After classification, the workflow routes to on-call with reminders. + * + * Run: + * export AXME_API_KEY= + * export AXME_AGENT_ADDRESS=incident-classifier-agent + * npx tsx examples/internal/escalation/agent.ts + */ +import { AxmeClient } from "@axme/axme"; + +const AXME_BASE_URL = process.env.AXME_BASE_URL ?? "https://api.cloud.axme.ai"; +const AXME_API_KEY = process.env.AXME_API_KEY ?? ""; +const AXME_AGENT_ADDRESS = process.env.AXME_AGENT_ADDRESS ?? "incident-classifier-agent"; + +if (!AXME_API_KEY) { console.error("AXME_API_KEY is required"); process.exit(1); } + +// --- Classification --- +const SEVERITY_SLA: Record = { P1: 5, P2: 15, P3: 60, P4: 240 }; +const CRITICAL_SERVICES = new Set(["api-gateway", "auth-service", "payments-processor"]); + +function classifyIncident(payload: Record): Record { + const incidentId = String(payload.incident_id ?? "?"); + const severity = String(payload.severity ?? "P3"); + const service = String(payload.service ?? ""); + const symptom = String(payload.symptom ?? ""); + const slaMinutes = SEVERITY_SLA[severity] ?? 60; + const isCritical = CRITICAL_SERVICES.has(service); + + const affected = service ? [service] : []; + if (isCritical) affected.push("downstream-dependents"); + + let escalationPath: string; + if (["P1", "P2"].includes(severity) && isCritical) escalationPath = "on-call → team-lead → engineering-director"; + else if (["P1", "P2"].includes(severity)) escalationPath = "on-call → team-lead"; + else escalationPath = "on-call"; + + return { + incident_id: incidentId, severity, is_critical_service: isCritical, + has_error_rate_signal: /error rate|5xx/i.test(symptom), + affected_components: affected, sla_minutes: slaMinutes, + escalation_path: escalationPath, + classification_note: `${incidentId}: ${severity} on '${service}' — SLA=${slaMinutes}m, escalation: ${escalationPath}`, + }; +} + +// --- Agent loop --- +const client = new AxmeClient({ apiKey: AXME_API_KEY, baseUrl: AXME_BASE_URL }); +console.log(`incident-classifier starting address=${AXME_AGENT_ADDRESS} binding=stream`); + +try { + for await (const delivery of client.listen(AXME_AGENT_ADDRESS)) { + const intentId = String(delivery.intent_id ?? ""); + if (!intentId) continue; + console.log(`received intent ${intentId}`); + + let intent: Record; + try { + const resp = await client.getIntent(intentId); + intent = (resp.intent ?? resp) as Record; + } catch (e) { console.error(`get_intent(${intentId}) failed:`, e); continue; } + + const status = String(intent.lifecycle_status ?? intent.status ?? "").toUpperCase(); + if (!["CREATED", "DELIVERED", "ACKNOWLEDGED", "IN_PROGRESS", "WAITING"].includes(status)) continue; + + const rawPayload = (intent.payload ?? {}) as Record; + const effectivePayload = (rawPayload.parent_payload ?? rawPayload) as Record; + const result = classifyIncident(effectivePayload); + + console.log(`classified incident ${intentId}: ${result.classification_note}`); + + try { + await client.resumeIntent(intentId, { action: "complete", ...result }, { ownerAgent: AXME_AGENT_ADDRESS }); + console.log(`resumed intent ${intentId}`); + } catch (e) { console.error(`resume_intent(${intentId}) failed:`, e); } + } +} catch (e) { + if ((e as Error).name !== "AbortError") throw e; +} diff --git a/examples/internal/notification/dotnet/NotificationAgent.cs b/examples/internal/notification/dotnet/NotificationAgent.cs new file mode 100644 index 0000000..49ac838 --- /dev/null +++ b/examples/internal/notification/dotnet/NotificationAgent.cs @@ -0,0 +1,55 @@ +using System.Text.Json.Nodes; +using Axme.Sdk; +using AxmeExamples; + +namespace AxmeExamples.Internal; + +public class NotificationAgent +{ + public static async Task Main(string[] args) + { + var baseUrl = SseHelper.Env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + var apiKey = SseHelper.Env("AXME_API_KEY"); + var addr = SseHelper.Env("AXME_AGENT_ADDRESS", "risk-assessment-agent"); + if (string.IsNullOrEmpty(apiKey)) { Console.Error.WriteLine("AXME_API_KEY is required"); return; } + + var client = new AxmeClient(new AxmeClientConfig { ApiKey = apiKey, BaseUrl = baseUrl }); + Console.WriteLine($"risk-assessment-agent starting address={addr}"); + var since = 0; + + while (true) + { + try + { + foreach (var d in await SseHelper.PollAgentStreamAsync(baseUrl, apiKey, addr, since)) + { + var id = SseHelper.Str(d, "intent_id"); + if (d["seq"]?.GetValue() is int seq) since = Math.Max(since, seq); + if (string.IsNullOrEmpty(id)) continue; + var intent = SseHelper.UnwrapIntent(await client.GetIntentAsync(id)); + if (!SseHelper.IsActionable(intent)) continue; + var p = SseHelper.EffectivePayload(intent); + + var ver = SseHelper.Str(p, "version", "0.0.0"); var env = SseHelper.Str(p, "environment"); + var cc = (int)(p["change_count"]?.GetValue() ?? 0); + var thresh = (int)(p["risk_threshold"]?.GetValue() ?? 30); + var parts = ver.Split('.'); int.TryParse(parts[0], out var major); var minor = parts.Length > 1 && int.TryParse(parts[1], out var m) ? m : 0; + + var score = 0; var factors = new List(); + if (major >= 4) { score += 40; factors.Add($"major bump +40"); } + else if (minor >= 5) { score += 15; factors.Add("minor bump +15"); } + if (cc > 30) { score += 30; factors.Add($"high changes +30"); } + if (env == "production") { score += 25; factors.Add("prod +25"); } + else if (env is "staging" or "canary") { score += 10; factors.Add("staging +10"); } + var level = score >= thresh ? "HIGH" : score >= thresh / 2 ? "MEDIUM" : "LOW"; + + var res = new JsonObject { ["action"] = "complete", ["risk_score"] = score, ["risk_level"] = level, + ["factors"] = new JsonArray(factors.Select(f => (JsonNode)JsonValue.Create(f)!).ToArray()) }; + await client.ResumeIntentAsync(id, res, new RequestOptions { OwnerAgent = addr }); + Console.WriteLine($"resumed {id} (risk={level} score={score})"); + } + } + catch { await Task.Delay(2000); } + } + } +} diff --git a/examples/internal/notification/go/agent.go b/examples/internal/notification/go/agent.go new file mode 100644 index 0000000..7c9c7ce --- /dev/null +++ b/examples/internal/notification/go/agent.go @@ -0,0 +1,92 @@ +// Risk Assessment Agent — internal/notification example (Go). +// +// Run: +// +// export AXME_API_KEY= +// export AXME_AGENT_ADDRESS=risk-assessment-agent +// go run examples/internal/notification/agent.go +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "strconv" + "strings" + + "github.com/AxmeAI/axme-sdk-go/axme" +) + +var ( + baseURL = envOr("AXME_BASE_URL", "https://api.cloud.axme.ai") + apiKey = os.Getenv("AXME_API_KEY") + agentAddress = envOr("AXME_AGENT_ADDRESS", "risk-assessment-agent") +) + +func envOr(k, d string) string { if v := os.Getenv(k); v != "" { return v }; return d } +func str(m map[string]any, k, d string) string { if v, ok := m[k].(string); ok { return v }; return d } + +func assessRisk(p map[string]any) (int, string, []string) { + version := str(p, "version", "0.0.0") + env := str(p, "environment", "") + changeCount := toInt(p["change_count"]) + threshold := toInt(p["risk_threshold"]) + if threshold == 0 { threshold = 30 } + + parts := strings.SplitN(version, ".", 3) + major, _ := strconv.Atoi(parts[0]) + minor := 0; if len(parts) > 1 { minor, _ = strconv.Atoi(parts[1]) } + + score := 0 + var factors []string + if major >= 4 { score += 40; factors = append(factors, fmt.Sprintf("major version bump (v%d.x) +40", major)) } else if minor >= 5 { score += 15; factors = append(factors, "significant minor version bump +15") } + if changeCount > 30 { score += 30; factors = append(factors, fmt.Sprintf("high change count (%d changes) +30", changeCount)) } + if env == "production" { score += 25; factors = append(factors, "production environment +25") } else if env == "staging" || env == "canary" { score += 10; factors = append(factors, "staging environment +10") } + + level := "LOW" + if score >= threshold { level = "HIGH" } else if score >= threshold/2 { level = "MEDIUM" } + return score, level, factors +} + +func toInt(v any) int { + switch n := v.(type) { + case float64: return int(n) + case int: return n + case string: i, _ := strconv.Atoi(n); return i + } + return 0 +} + +func main() { + if apiKey == "" { log.Fatal("AXME_API_KEY is required") } + client, err := axme.NewClient(axme.ClientConfig{APIKey: apiKey, BaseURL: baseURL}) + if err != nil { log.Fatalf("client init: %v", err) } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + log.Printf("risk-assessment-agent starting address=%s binding=stream", agentAddress) + + intents, errs := client.Listen(ctx, agentAddress, axme.ListenOptions{}) + for { + select { + case d, ok := <-intents: + if !ok { return } + id := str(d, "intent_id", ""); if id == "" { continue } + resp, _ := client.GetIntent(ctx, id, axme.RequestOptions{}) + intent, _ := resp["intent"].(map[string]any); if intent == nil { intent = resp } + status := strings.ToUpper(str(intent, "lifecycle_status", str(intent, "status", ""))) + if !map[string]bool{"CREATED": true, "DELIVERED": true, "ACKNOWLEDGED": true, "IN_PROGRESS": true, "WAITING": true}[status] { continue } + raw, _ := intent["payload"].(map[string]any); if raw == nil { raw = map[string]any{} } + eff, _ := raw["parent_payload"].(map[string]any); if eff == nil { eff = raw } + + score, level, factors := assessRisk(eff) + log.Printf("risk assessment for %s: score=%d level=%s", id, score, level) + client.ResumeIntent(ctx, id, map[string]any{"action": "complete", "risk_score": score, "risk_level": level, "factors": factors}, axme.RequestOptions{OwnerAgent: agentAddress}) + log.Printf("resumed %s (risk=%s)", id, level) + case err, ok := <-errs: if !ok { return }; log.Printf("error: %v", err) + case <-ctx.Done(): return + } + } +} diff --git a/examples/internal/notification/java/NotificationAgent.java b/examples/internal/notification/java/NotificationAgent.java new file mode 100644 index 0000000..419c2bd --- /dev/null +++ b/examples/internal/notification/java/NotificationAgent.java @@ -0,0 +1,48 @@ +package ai.axme.examples.internal; + +import ai.axme.examples.SseHelper; +import dev.axme.sdk.*; +import java.util.*; + +/** Risk Assessment Agent — internal/notification example (Java). */ +public class NotificationAgent { + static final String BASE = SseHelper.env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + static final String KEY = System.getenv("AXME_API_KEY"); + static final String ADDR = SseHelper.env("AXME_AGENT_ADDRESS", "risk-assessment-agent"); + + public static void main(String[] args) throws Exception { + if (KEY == null || KEY.isEmpty()) { System.err.println("AXME_API_KEY is required"); System.exit(1); } + AxmeClient client = new AxmeClient(new AxmeClientConfig(BASE, KEY)); + System.out.printf("risk-assessment-agent starting address=%s%n", ADDR); + int since = 0; + while (true) { + try { + for (Map d : SseHelper.pollAgentStream(BASE, KEY, ADDR, since, 15)) { + String id = SseHelper.str(d, "intent_id"); Number seq = (Number) d.get("seq"); + if (seq != null) since = Math.max(since, seq.intValue()); if (id.isEmpty()) continue; + Map intent = SseHelper.unwrapIntent(client.getIntent(id, RequestOptions.none())); + if (!SseHelper.isActionable(intent)) continue; + Map p = SseHelper.effectivePayload(intent); + + String ver = SseHelper.str(p, "version", "0.0.0"), env = SseHelper.str(p, "environment"); + int cc = p.get("change_count") instanceof Number n ? n.intValue() : 0; + int thresh = p.get("risk_threshold") instanceof Number n ? n.intValue() : 30; + String[] parts = ver.split("\\."); int major = parts.length > 0 ? Integer.parseInt(parts[0]) : 0; + int minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; + + int score = 0; List factors = new ArrayList<>(); + if (major >= 4) { score += 40; factors.add("major bump +40"); } + else if (minor >= 5) { score += 15; factors.add("minor bump +15"); } + if (cc > 30) { score += 30; factors.add("high changes +30"); } + if ("production".equals(env)) { score += 25; factors.add("prod +25"); } + else if ("staging".equals(env) || "canary".equals(env)) { score += 10; factors.add("staging +10"); } + String level = score >= thresh ? "HIGH" : score >= thresh / 2 ? "MEDIUM" : "LOW"; + + client.resumeIntent(id, Map.of("action", "complete", "risk_score", score, "risk_level", level, "factors", factors), + SseHelper.withOwner(ADDR)); + System.out.printf("resumed %s (risk=%s score=%d)%n", id, level, score); + } + } catch (Exception e) { Thread.sleep(2000); } + } + } +} diff --git a/examples/internal/notification/agent.py b/examples/internal/notification/python/agent.py similarity index 100% rename from examples/internal/notification/agent.py rename to examples/internal/notification/python/agent.py diff --git a/examples/internal/notification/typescript/agent.ts b/examples/internal/notification/typescript/agent.ts new file mode 100644 index 0000000..03b67b3 --- /dev/null +++ b/examples/internal/notification/typescript/agent.ts @@ -0,0 +1,88 @@ +/** + * Risk Assessment Agent — internal/notification example (TypeScript). + * + * Assesses deployment risk. If risk exceeds threshold, signals HIGH risk + * and the workflow triggers the built-in notification step. + * + * Run: + * export AXME_API_KEY= + * export AXME_AGENT_ADDRESS=risk-assessment-agent + * npx tsx examples/internal/notification/agent.ts + */ +import { AxmeClient } from "@axme/axme"; + +const AXME_BASE_URL = process.env.AXME_BASE_URL ?? "https://api.cloud.axme.ai"; +const AXME_API_KEY = process.env.AXME_API_KEY ?? ""; +const AXME_AGENT_ADDRESS = process.env.AXME_AGENT_ADDRESS ?? "risk-assessment-agent"; + +if (!AXME_API_KEY) { console.error("AXME_API_KEY is required"); process.exit(1); } + +// --- Risk scoring --- +const MAJOR_BUMP_WEIGHT = 40; +const MINOR_BUMP_WEIGHT = 15; +const HIGH_CHANGE_WEIGHT = 30; +const PROD_ENV_WEIGHT = 25; +const STAGING_ENV_WEIGHT = 10; + +function parseSemver(v: string): [number, number, number] { + const parts = v.trim().split("."); + return [parseInt(parts[0]) || 0, parseInt(parts[1]) || 0, parseInt(parts[2]) || 0]; +} + +function assessRisk(payload: Record): { score: number; level: string; factors: string[] } { + const version = String(payload.version ?? "0.0.0"); + const environment = String(payload.environment ?? ""); + const changeCount = Number(payload.change_count ?? 0); + const threshold = Number(payload.risk_threshold ?? 30); + const [major, minor] = parseSemver(version); + + let score = 0; + const factors: string[] = []; + + if (major >= 4) { score += MAJOR_BUMP_WEIGHT; factors.push(`major version bump (v${major}.x) +${MAJOR_BUMP_WEIGHT}`); } + else if (minor >= 5) { score += MINOR_BUMP_WEIGHT; factors.push(`significant minor version bump +${MINOR_BUMP_WEIGHT}`); } + + if (changeCount > 30) { score += HIGH_CHANGE_WEIGHT; factors.push(`high change count (${changeCount} changes) +${HIGH_CHANGE_WEIGHT}`); } + + if (environment === "production") { score += PROD_ENV_WEIGHT; factors.push(`production environment +${PROD_ENV_WEIGHT}`); } + else if (["staging", "canary"].includes(environment)) { score += STAGING_ENV_WEIGHT; factors.push(`staging environment +${STAGING_ENV_WEIGHT}`); } + + const level = score >= threshold ? "HIGH" : score >= threshold / 2 ? "MEDIUM" : "LOW"; + return { score, level, factors }; +} + +// --- Agent loop --- +const client = new AxmeClient({ apiKey: AXME_API_KEY, baseUrl: AXME_BASE_URL }); +console.log(`risk-assessment-agent starting address=${AXME_AGENT_ADDRESS} binding=stream`); + +try { + for await (const delivery of client.listen(AXME_AGENT_ADDRESS)) { + const intentId = String(delivery.intent_id ?? ""); + if (!intentId) continue; + console.log(`received intent ${intentId}`); + + let intent: Record; + try { + const resp = await client.getIntent(intentId); + intent = (resp.intent ?? resp) as Record; + } catch (e) { console.error(`get_intent(${intentId}) failed:`, e); continue; } + + const status = String(intent.lifecycle_status ?? intent.status ?? "").toUpperCase(); + if (!["CREATED", "DELIVERED", "ACKNOWLEDGED", "IN_PROGRESS", "WAITING"].includes(status)) continue; + + const rawPayload = (intent.payload ?? {}) as Record; + const effectivePayload = (rawPayload.parent_payload ?? rawPayload) as Record; + const { score, level, factors } = assessRisk(effectivePayload); + + console.log(`risk assessment for ${intentId}: score=${score} level=${level} factors=${factors.length}`); + + try { + await client.resumeIntent(intentId, { + action: "complete", risk_score: score, risk_level: level, factors, + }, { ownerAgent: AXME_AGENT_ADDRESS }); + console.log(`resumed intent ${intentId} (risk=${level} score=${score})`); + } catch (e) { console.error(`resume_intent(${intentId}) failed:`, e); } + } +} catch (e) { + if ((e as Error).name !== "AbortError") throw e; +} diff --git a/examples/model-a/fire-and-forget/dotnet/FireAndForgetInitiator.cs b/examples/model-a/fire-and-forget/dotnet/FireAndForgetInitiator.cs new file mode 100644 index 0000000..32c7a2e --- /dev/null +++ b/examples/model-a/fire-and-forget/dotnet/FireAndForgetInitiator.cs @@ -0,0 +1,41 @@ +using System.Text.Json.Nodes; +using Axme.Sdk; +using AxmeExamples; + +namespace AxmeExamples.ModelA; + +public class FireAndForgetInitiator +{ + public static async Task Main(string[] args) + { + var baseUrl = SseHelper.Env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + var apiKey = SseHelper.Env("AXME_API_KEY"); + var toAgent = SseHelper.Env("AXME_TO_AGENT"); + if (string.IsNullOrEmpty(apiKey)) { Console.Error.WriteLine("AXME_API_KEY is required"); return; } + if (string.IsNullOrEmpty(toAgent)) { Console.Error.WriteLine("AXME_TO_AGENT is required"); return; } + + var client = new AxmeClient(new AxmeClientConfig { ApiKey = apiKey, BaseUrl = baseUrl }); + Console.WriteLine($"firing intent to {toAgent} ..."); + + var created = await client.CreateIntentAsync(new JsonObject + { + ["intent_type"] = "intent.compliance.check.v1", + ["from_agent"] = "initiator://fire-and-forget", ["to_agent"] = toAgent, + ["reply_to"] = "initiator://fire-and-forget", + ["correlation_id"] = Guid.NewGuid().ToString(), + ["payload"] = new JsonObject { ["change_id"] = "CHG-FIRE-FORGET-001", ["service"] = "payments-service", + ["version"] = "2.1.0", ["environment"] = "staging", ["change_type"] = "config_update", ["risk_level"] = "medium" } + }); + var intentId = created["intent_id"]?.ToString() ?? ""; + Console.WriteLine($"intent sent: {intentId} — disconnecting"); + Console.WriteLine("doing other work for 15 seconds..."); + await Task.Delay(15000); + + Console.WriteLine("checking intent status..."); + var resp = await client.GetIntentAsync(intentId); + var intent = SseHelper.UnwrapIntent(resp); + var status = SseHelper.Str(intent, "lifecycle_status") is { Length: > 0 } s ? s : SseHelper.Str(intent, "status"); + Console.WriteLine($"intent {intentId} → {status}"); + if (status is "COMPLETED" or "IN_PROGRESS") Console.WriteLine("SUCCESS — fire-and-forget verified"); + } +} diff --git a/examples/model-a/fire-and-forget/go/initiator.go b/examples/model-a/fire-and-forget/go/initiator.go new file mode 100644 index 0000000..fef2ea0 --- /dev/null +++ b/examples/model-a/fire-and-forget/go/initiator.go @@ -0,0 +1,75 @@ +// Model A — Fire and Forget: send intent, disconnect, check later (Go). +// +// Run: +// +// AXME_API_KEY= AXME_TO_AGENT=agent://org/ws/compliance-checker-agent \ +// go run examples/model-a/fire-and-forget/initiator.go +package main + +import ( + "context" + "crypto/rand" + "fmt" + "log" + "os" + "time" + + "github.com/AxmeAI/axme-sdk-go/axme" +) + +func newUUID() string { + b := make([]byte, 16) + rand.Read(b) + return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) +} + +func envOr(k, d string) string { if v := os.Getenv(k); v != "" { return v }; return d } + +func main() { + apiKey := os.Getenv("AXME_API_KEY") + baseURL := envOr("AXME_BASE_URL", "https://api.cloud.axme.ai") + toAgent := os.Getenv("AXME_TO_AGENT") + + if apiKey == "" { log.Fatal("AXME_API_KEY is required") } + if toAgent == "" { log.Fatal("AXME_TO_AGENT is required") } + + client, err := axme.NewClient(axme.ClientConfig{APIKey: apiKey, BaseURL: baseURL}) + if err != nil { log.Fatalf("client init: %v", err) } + ctx := context.Background() + + log.Printf("firing intent to %s ...", toAgent) + created, err := client.CreateIntent(ctx, map[string]any{ + "intent_type": "intent.compliance.check.v1", + "correlation_id": newUUID(), + "from_agent": "initiator://fire-and-forget", + "to_agent": toAgent, + "reply_to": "initiator://fire-and-forget", + "payload": map[string]any{ + "change_id": "CHG-FIRE-FORGET-001", "service": "payments-service", "version": "2.1.0", + "environment": "staging", "change_type": "config_update", "risk_level": "medium", + }, + }, axme.RequestOptions{}) + if err != nil { log.Fatalf("create_intent failed: %v", err) } + + intentID, _ := created["intent_id"].(string) + log.Printf("intent sent: %s — disconnecting", intentID) + log.Println("doing other work for 15 seconds...") + time.Sleep(15 * time.Second) + + log.Println("checking intent status...") + resp, err := client.GetIntent(ctx, intentID, axme.RequestOptions{}) + if err != nil { log.Fatalf("get_intent failed: %v", err) } + + intent, _ := resp["intent"].(map[string]any) + if intent == nil { intent = resp } + status := "" + if s, ok := intent["lifecycle_status"].(string); ok { status = s } + if status == "" { status, _ = intent["status"].(string) } + + log.Printf("intent %s → %s", intentID, status) + if status == "COMPLETED" || status == "IN_PROGRESS" { + log.Println("SUCCESS — fire-and-forget pattern verified") + } else { + log.Printf("status: %s (agent may still be processing)", status) + } +} diff --git a/examples/model-a/fire-and-forget/java/FireAndForgetInitiator.java b/examples/model-a/fire-and-forget/java/FireAndForgetInitiator.java new file mode 100644 index 0000000..63d6da6 --- /dev/null +++ b/examples/model-a/fire-and-forget/java/FireAndForgetInitiator.java @@ -0,0 +1,41 @@ +package ai.axme.examples.modela; + +import ai.axme.examples.SseHelper; +import dev.axme.sdk.*; +import java.util.*; + +/** Model A — Fire and Forget (Java). */ +public class FireAndForgetInitiator { + public static void main(String[] args) throws Exception { + String apiKey = System.getenv("AXME_API_KEY"); + String baseUrl = SseHelper.env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + String toAgent = System.getenv("AXME_TO_AGENT"); + if (apiKey == null || apiKey.isEmpty()) { System.err.println("AXME_API_KEY is required"); System.exit(1); } + if (toAgent == null || toAgent.isEmpty()) { System.err.println("AXME_TO_AGENT is required"); System.exit(1); } + + AxmeClient client = new AxmeClient(new AxmeClientConfig(baseUrl, apiKey)); + System.out.printf("firing intent to %s ...%n", toAgent); + + Map created = client.createIntent(Map.of( + "intent_type", "intent.compliance.check.v1", + "from_agent", "initiator://fire-and-forget", "to_agent", toAgent, + "reply_to", "initiator://fire-and-forget", + "correlation_id", UUID.randomUUID().toString(), + "payload", Map.of("change_id", "CHG-FIRE-FORGET-001", "service", "payments-service", + "version", "2.1.0", "environment", "staging", "change_type", "config_update", "risk_level", "medium") + ), RequestOptions.none()); + String intentId = (String) created.get("intent_id"); + System.out.printf("intent sent: %s — disconnecting%n", intentId); + + System.out.println("doing other work for 15 seconds..."); + Thread.sleep(15000); + + System.out.println("checking intent status..."); + Map resp = client.getIntent(intentId, RequestOptions.none()); + Map intent = SseHelper.unwrapIntent(resp); + String status = SseHelper.str(intent, "lifecycle_status"); + if (status.isEmpty()) status = SseHelper.str(intent, "status"); + System.out.printf("intent %s → %s%n", intentId, status); + if ("COMPLETED".equals(status) || "IN_PROGRESS".equals(status)) System.out.println("SUCCESS — fire-and-forget verified"); + } +} diff --git a/examples/model-a/fire-and-forget/initiator.py b/examples/model-a/fire-and-forget/python/initiator.py similarity index 100% rename from examples/model-a/fire-and-forget/initiator.py rename to examples/model-a/fire-and-forget/python/initiator.py diff --git a/examples/model-a/fire-and-forget/typescript/initiator.ts b/examples/model-a/fire-and-forget/typescript/initiator.ts new file mode 100644 index 0000000..a675365 --- /dev/null +++ b/examples/model-a/fire-and-forget/typescript/initiator.ts @@ -0,0 +1,48 @@ +/** + * Model A — Fire and Forget: send intent, disconnect, check later (TypeScript). + * + * Run: + * AXME_API_KEY= AXME_TO_AGENT=agent://org/ws/compliance-checker-agent \ + * npx tsx examples/model-a/fire-and-forget/initiator.ts + */ +import { AxmeClient } from "@axme/axme"; + +const AXME_BASE_URL = process.env.AXME_BASE_URL ?? "https://api.cloud.axme.ai"; +const AXME_API_KEY = process.env.AXME_API_KEY ?? ""; +const AXME_TO_AGENT = process.env.AXME_TO_AGENT ?? ""; + +if (!AXME_API_KEY) { console.error("AXME_API_KEY is required"); process.exit(1); } +if (!AXME_TO_AGENT) { console.error("AXME_TO_AGENT is required"); process.exit(1); } + +const client = new AxmeClient({ apiKey: AXME_API_KEY, baseUrl: AXME_BASE_URL }); + +// 1. Fire +console.log(`firing intent to ${AXME_TO_AGENT} ...`); +const intentId = await client.sendIntent({ + intent_type: "intent.compliance.check.v1", + from_agent: "initiator://fire-and-forget", + to_agent: AXME_TO_AGENT, + reply_to: "initiator://fire-and-forget", + payload: { + change_id: "CHG-FIRE-FORGET-001", service: "payments-service", version: "2.1.0", + environment: "staging", change_type: "config_update", risk_level: "medium", + }, +}); +console.log(`intent sent: ${intentId} — disconnecting`); + +// 2. Forget — do other work +console.log("doing other work for 15 seconds..."); +await new Promise(r => setTimeout(r, 15_000)); + +// 3. Check result +console.log("checking intent status..."); +const resp = await client.getIntent(intentId); +const intentData = (resp.intent ?? resp) as Record; +const finalStatus = String(intentData.lifecycle_status ?? intentData.status ?? "?"); +console.log(`intent ${intentId} → ${finalStatus}`); + +if (["COMPLETED", "IN_PROGRESS"].includes(finalStatus)) { + console.log("SUCCESS — fire-and-forget pattern verified"); +} else { + console.log(`status: ${finalStatus} (agent may still be processing)`); +} diff --git a/examples/model-a/manual-multi-step/dotnet/ManualMultiStepInitiator.cs b/examples/model-a/manual-multi-step/dotnet/ManualMultiStepInitiator.cs new file mode 100644 index 0000000..59a7b55 --- /dev/null +++ b/examples/model-a/manual-multi-step/dotnet/ManualMultiStepInitiator.cs @@ -0,0 +1,70 @@ +using System.Text.Json.Nodes; +using Axme.Sdk; +using AxmeExamples; + +namespace AxmeExamples.ModelA; + +public class ManualMultiStepInitiator +{ + private static async Task WaitForDone(AxmeClient client, string intentId, int timeoutSec = 60) + { + var since = 0; var deadline = DateTime.UtcNow.AddSeconds(timeoutSec); + while (DateTime.UtcNow < deadline) + { + var events = await client.ListIntentEventsAsync(intentId, since); + foreach (var evt in events["events"]?.AsArray() ?? []) + { + var st = evt?["status"]?.ToString() ?? ""; + if (evt?["seq"]?.GetValue() is int seq) since = Math.Max(since, seq); + if (st is "COMPLETED" or "IN_PROGRESS" or "FAILED" or "CANCELED" or "TIMED_OUT") return st; + } + await Task.Delay(1000); + } + return "UNKNOWN"; + } + + public static async Task Main(string[] args) + { + var baseUrl = SseHelper.Env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + var apiKey = SseHelper.Env("AXME_API_KEY"); + var agent1 = SseHelper.Env("AXME_AGENT_1"); var agent2 = SseHelper.Env("AXME_AGENT_2"); + if (string.IsNullOrEmpty(apiKey)) { Console.Error.WriteLine("AXME_API_KEY is required"); return; } + if (string.IsNullOrEmpty(agent1) || string.IsNullOrEmpty(agent2)) { Console.Error.WriteLine("AXME_AGENT_1 and AXME_AGENT_2 required"); return; } + + var client = new AxmeClient(new AxmeClientConfig { ApiKey = apiKey, BaseUrl = baseUrl }); + + var payload = new JsonObject { ["change_id"] = "CHG-MULTI-STEP-001", ["service"] = "api-gateway", + ["version"] = "5.0.0", ["environment"] = "staging", ["change_type"] = "config_update", + ["risk_level"] = "medium", ["risk_threshold"] = 30 }; + + // Step 1 + Console.WriteLine($"STEP 1: compliance check → {agent1}"); + var c1 = await client.CreateIntentAsync(new JsonObject { ["intent_type"] = "intent.compliance.check.v1", + ["from_agent"] = "initiator://manual-multi-step", ["to_agent"] = agent1, + ["correlation_id"] = Guid.NewGuid().ToString(), ["payload"] = payload.DeepClone() }); + var s1id = c1["intent_id"]?.ToString() ?? ""; + Console.WriteLine($"step 1 intent: {s1id} — waiting..."); + var s1 = await WaitForDone(client, s1id); + Console.WriteLine($"step 1 → {s1}"); + + if (s1 is not ("COMPLETED" or "IN_PROGRESS")) { Console.Error.WriteLine($"step 1 failed ({s1}) — aborting"); return; } + + // Step 2 + Console.WriteLine($"STEP 2: risk assessment → {agent2}"); + var p2 = payload.DeepClone().AsObject(); + p2["compliance_result"] = "passed"; p2["compliance_intent_id"] = s1id; + var c2 = await client.CreateIntentAsync(new JsonObject { ["intent_type"] = "intent.risk.assessment.v1", + ["from_agent"] = "initiator://manual-multi-step", ["to_agent"] = agent2, + ["correlation_id"] = Guid.NewGuid().ToString(), ["payload"] = p2 }); + var s2id = c2["intent_id"]?.ToString() ?? ""; + Console.WriteLine($"step 2 intent: {s2id} — waiting..."); + var s2 = await WaitForDone(client, s2id); + Console.WriteLine($"step 2 → {s2}"); + + Console.WriteLine("\n=== Manual Multi-Step Pipeline ==="); + Console.WriteLine($" Step 1 (compliance): {s1} id={s1id}"); + Console.WriteLine($" Step 2 (risk): {s2} id={s2id}"); + var ok = s1 is "COMPLETED" or "IN_PROGRESS" && s2 is "COMPLETED" or "IN_PROGRESS"; + Console.WriteLine($" Result: {(ok ? "ALL STEPS PASSED" : "PIPELINE FAILED")}"); + } +} diff --git a/examples/model-a/manual-multi-step/go/initiator.go b/examples/model-a/manual-multi-step/go/initiator.go new file mode 100644 index 0000000..41edd8a --- /dev/null +++ b/examples/model-a/manual-multi-step/go/initiator.go @@ -0,0 +1,111 @@ +// Model A — Manual Multi-Step: chain two agents sequentially (Go). +// +// Run: +// +// AXME_API_KEY= \ +// AXME_AGENT_1=agent://org/ws/compliance-checker-agent \ +// AXME_AGENT_2=agent://org/ws/risk-assessment-agent \ +// go run examples/model-a/manual-multi-step/initiator.go +package main + +import ( + "context" + "crypto/rand" + "fmt" + "log" + "os" + "time" + + "github.com/AxmeAI/axme-sdk-go/axme" +) + +func envOr(k, d string) string { if v := os.Getenv(k); v != "" { return v }; return d } + +func waitForDone(ctx context.Context, client *axme.Client, intentID string, timeout time.Duration) string { + deadline := time.After(timeout) + since := 0 + sincePtr := &since + for { + select { + case <-deadline: return "TIMEOUT" + default: + } + events, err := client.ListIntentEvents(ctx, intentID, sincePtr, axme.RequestOptions{}) + if err != nil { time.Sleep(time.Second); continue } + evtList, _ := events["events"].([]any) + for _, e := range evtList { + evt, _ := e.(map[string]any) + status, _ := evt["status"].(string) + if seq, ok := evt["seq"].(float64); ok { since = int(seq) } + if status == "COMPLETED" || status == "IN_PROGRESS" || status == "FAILED" || status == "CANCELED" || status == "TIMED_OUT" { return status } + } + time.Sleep(time.Second) + } +} + +func main() { + apiKey := os.Getenv("AXME_API_KEY") + baseURL := envOr("AXME_BASE_URL", "https://api.cloud.axme.ai") + agent1 := os.Getenv("AXME_AGENT_1") + agent2 := os.Getenv("AXME_AGENT_2") + + if apiKey == "" { log.Fatal("AXME_API_KEY is required") } + if agent1 == "" || agent2 == "" { log.Fatal("AXME_AGENT_1 and AXME_AGENT_2 are required") } + + client, err := axme.NewClient(axme.ClientConfig{APIKey: apiKey, BaseURL: baseURL}) + if err != nil { log.Fatalf("client init: %v", err) } + ctx := context.Background() + newUUID := func() string { + b := make([]byte, 16) + rand.Read(b) + return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) + } + + changePayload := map[string]any{ + "change_id": "CHG-MULTI-STEP-001", "service": "api-gateway", "version": "5.0.0", + "environment": "staging", "change_type": "config_update", "risk_level": "medium", "risk_threshold": 30, + } + + // Step 1 + log.Printf("STEP 1: compliance check → %s", agent1) + c1, err := client.CreateIntent(ctx, map[string]any{ + "intent_type": "intent.compliance.check.v1", "correlation_id": newUUID(), + "from_agent": "initiator://manual-multi-step", "to_agent": agent1, "payload": changePayload, + }, axme.RequestOptions{}) + if err != nil { log.Fatalf("create step1 failed: %v", err) } + step1ID, _ := c1["intent_id"].(string) + log.Printf("step 1 intent: %s — waiting...", step1ID) + step1Status := waitForDone(ctx, client, step1ID, 60*time.Second) + log.Printf("step 1 → %s", step1Status) + + if step1Status != "COMPLETED" && step1Status != "IN_PROGRESS" { + log.Fatalf("step 1 failed (%s) — aborting pipeline", step1Status) + } + + // Step 2 + log.Printf("STEP 2: risk assessment → %s", agent2) + step2Payload := map[string]any{} + for k, v := range changePayload { step2Payload[k] = v } + step2Payload["compliance_result"] = "passed" + step2Payload["compliance_intent_id"] = step1ID + + c2, err := client.CreateIntent(ctx, map[string]any{ + "intent_type": "intent.risk.assessment.v1", "correlation_id": newUUID(), + "from_agent": "initiator://manual-multi-step", "to_agent": agent2, "payload": step2Payload, + }, axme.RequestOptions{}) + if err != nil { log.Fatalf("create step2 failed: %v", err) } + step2ID, _ := c2["intent_id"].(string) + log.Printf("step 2 intent: %s — waiting...", step2ID) + step2Status := waitForDone(ctx, client, step2ID, 60*time.Second) + log.Printf("step 2 → %s", step2Status) + + log.Println("") + log.Println("=== Manual Multi-Step Pipeline ===") + log.Printf(" Step 1 (compliance): %s id=%s", step1Status, step1ID) + log.Printf(" Step 2 (risk): %s id=%s", step2Status, step2ID) + if (step1Status == "COMPLETED" || step1Status == "IN_PROGRESS") && (step2Status == "COMPLETED" || step2Status == "IN_PROGRESS") { + log.Println(" Result: ALL STEPS PASSED") + } else { + log.Println(" Result: PIPELINE FAILED") + } +} diff --git a/examples/model-a/manual-multi-step/java/ManualMultiStepInitiator.java b/examples/model-a/manual-multi-step/java/ManualMultiStepInitiator.java new file mode 100644 index 0000000..f0d1847 --- /dev/null +++ b/examples/model-a/manual-multi-step/java/ManualMultiStepInitiator.java @@ -0,0 +1,65 @@ +package ai.axme.examples.modela; + +import ai.axme.examples.SseHelper; +import dev.axme.sdk.*; +import java.util.*; + +/** Model A — Manual Multi-Step (Java). */ +public class ManualMultiStepInitiator { + public static void main(String[] args) throws Exception { + String apiKey = System.getenv("AXME_API_KEY"); + String baseUrl = SseHelper.env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + String agent1 = System.getenv("AXME_AGENT_1"), agent2 = System.getenv("AXME_AGENT_2"); + if (apiKey == null || apiKey.isEmpty()) { System.err.println("AXME_API_KEY is required"); System.exit(1); } + if (agent1 == null || agent2 == null || agent1.isEmpty() || agent2.isEmpty()) { + System.err.println("AXME_AGENT_1 and AXME_AGENT_2 required"); System.exit(1); + } + + AxmeClient client = new AxmeClient(new AxmeClientConfig(baseUrl, apiKey)); + Map payload = Map.of("change_id", "CHG-MULTI-STEP-001", "service", "api-gateway", + "version", "5.0.0", "environment", "staging", "change_type", "config_update", + "risk_level", "medium", "risk_threshold", 30); + + // Step 1 + System.out.printf("STEP 1: compliance check → %s%n", agent1); + Map c1 = client.createIntent(Map.of("intent_type", "intent.compliance.check.v1", + "from_agent", "initiator://manual-multi-step", "to_agent", agent1, + "correlation_id", UUID.randomUUID().toString(), "payload", payload), RequestOptions.none()); + String s1id = (String) c1.get("intent_id"); + String s1 = waitForDone(client, s1id, 60_000); + System.out.printf("step 1 → %s%n", s1); + if (!"COMPLETED".equals(s1) && !"IN_PROGRESS".equals(s1)) { System.err.println("step 1 failed — aborting"); return; } + + // Step 2 + System.out.printf("STEP 2: risk assessment → %s%n", agent2); + Map p2 = new HashMap<>(payload); + p2.put("compliance_result", "passed"); p2.put("compliance_intent_id", s1id); + Map c2 = client.createIntent(Map.of("intent_type", "intent.risk.assessment.v1", + "from_agent", "initiator://manual-multi-step", "to_agent", agent2, + "correlation_id", UUID.randomUUID().toString(), "payload", p2), RequestOptions.none()); + String s2id = (String) c2.get("intent_id"); + String s2 = waitForDone(client, s2id, 60_000); + System.out.printf("step 2 → %s%n", s2); + + System.out.println("\n=== Manual Multi-Step Pipeline ==="); + System.out.printf(" Step 1 (compliance): %s id=%s%n", s1, s1id); + System.out.printf(" Step 2 (risk): %s id=%s%n", s2, s2id); + boolean ok = ("COMPLETED".equals(s1) || "IN_PROGRESS".equals(s1)) && ("COMPLETED".equals(s2) || "IN_PROGRESS".equals(s2)); + System.out.println(" Result: " + (ok ? "ALL STEPS PASSED" : "PIPELINE FAILED")); + } + + @SuppressWarnings("unchecked") + static String waitForDone(AxmeClient client, String intentId, long timeoutMs) throws Exception { + int since = 0; long deadline = System.currentTimeMillis() + timeoutMs; + while (System.currentTimeMillis() < deadline) { + Map events = client.listIntentEvents(intentId, since, RequestOptions.none()); + for (Map evt : (List>) events.getOrDefault("events", List.of())) { + String status = SseHelper.str(evt, "status"); + if (evt.get("seq") instanceof Number n) since = Math.max(since, n.intValue()); + if (Set.of("COMPLETED", "IN_PROGRESS", "FAILED", "CANCELED", "TIMED_OUT").contains(status)) return status; + } + Thread.sleep(1000); + } + return "UNKNOWN"; + } +} diff --git a/examples/model-a/manual-multi-step/initiator.py b/examples/model-a/manual-multi-step/python/initiator.py similarity index 100% rename from examples/model-a/manual-multi-step/initiator.py rename to examples/model-a/manual-multi-step/python/initiator.py diff --git a/examples/model-a/manual-multi-step/typescript/initiator.ts b/examples/model-a/manual-multi-step/typescript/initiator.ts new file mode 100644 index 0000000..0b1a5fd --- /dev/null +++ b/examples/model-a/manual-multi-step/typescript/initiator.ts @@ -0,0 +1,70 @@ +/** + * Model A — Manual Multi-Step: chain two agents sequentially (TypeScript). + * + * Run: + * AXME_API_KEY= \ + * AXME_AGENT_1=agent://org/ws/compliance-checker-agent \ + * AXME_AGENT_2=agent://org/ws/risk-assessment-agent \ + * npx tsx examples/model-a/manual-multi-step/initiator.ts + */ +import { AxmeClient } from "@axme/axme"; + +const AXME_BASE_URL = process.env.AXME_BASE_URL ?? "https://api.cloud.axme.ai"; +const AXME_API_KEY = process.env.AXME_API_KEY ?? ""; +const AGENT_1 = process.env.AXME_AGENT_1 ?? ""; +const AGENT_2 = process.env.AXME_AGENT_2 ?? ""; + +if (!AXME_API_KEY) { console.error("AXME_API_KEY is required"); process.exit(1); } +if (!AGENT_1 || !AGENT_2) { console.error("AXME_AGENT_1 and AXME_AGENT_2 are required"); process.exit(1); } + +const client = new AxmeClient({ apiKey: AXME_API_KEY, baseUrl: AXME_BASE_URL }); + +async function waitForDone(intentId: string, timeout = 60_000): Promise { + for await (const event of client.observe(intentId, { timeoutMs: timeout })) { + const status = String(event.status ?? ""); + if (["COMPLETED", "IN_PROGRESS", "FAILED", "CANCELED", "TIMED_OUT"].includes(status)) return status; + } + return "UNKNOWN"; +} + +const changePayload = { + change_id: "CHG-MULTI-STEP-001", service: "api-gateway", version: "5.0.0", + environment: "staging", change_type: "config_update", risk_level: "medium", risk_threshold: 30, +}; + +// Step 1: Compliance Check +console.log(`STEP 1: compliance check → ${AGENT_1}`); +const step1Id = await client.sendIntent({ + intent_type: "intent.compliance.check.v1", + from_agent: "initiator://manual-multi-step", to_agent: AGENT_1, payload: changePayload, +}); +console.log(`step 1 intent: ${step1Id} — waiting...`); +const step1Status = await waitForDone(step1Id); +console.log(`step 1 → ${step1Status}`); + +if (!["COMPLETED", "IN_PROGRESS"].includes(step1Status)) { + console.error(`step 1 failed (${step1Status}) — aborting pipeline`); + process.exit(1); +} + +// Step 2: Risk Assessment +console.log(`STEP 2: risk assessment → ${AGENT_2}`); +const step2Id = await client.sendIntent({ + intent_type: "intent.risk.assessment.v1", + from_agent: "initiator://manual-multi-step", to_agent: AGENT_2, + payload: { ...changePayload, compliance_result: "passed", compliance_intent_id: step1Id }, +}); +console.log(`step 2 intent: ${step2Id} — waiting...`); +const step2Status = await waitForDone(step2Id); +console.log(`step 2 → ${step2Status}`); + +// Summary +console.log(""); +console.log("=== Manual Multi-Step Pipeline ==="); +console.log(` Step 1 (compliance): ${step1Status} id=${step1Id}`); +console.log(` Step 2 (risk): ${step2Status} id=${step2Id}`); +if (["COMPLETED", "IN_PROGRESS"].includes(step1Status) && ["COMPLETED", "IN_PROGRESS"].includes(step2Status)) { + console.log(" Result: ALL STEPS PASSED"); +} else { + console.log(" Result: PIPELINE FAILED"); +} diff --git a/examples/model-a/simple-request/dotnet/SimpleRequestInitiator.cs b/examples/model-a/simple-request/dotnet/SimpleRequestInitiator.cs new file mode 100644 index 0000000..8d824b5 --- /dev/null +++ b/examples/model-a/simple-request/dotnet/SimpleRequestInitiator.cs @@ -0,0 +1,52 @@ +using System.Text.Json.Nodes; +using Axme.Sdk; +using AxmeExamples; + +namespace AxmeExamples.ModelA; + +public class SimpleRequestInitiator +{ + public static async Task Main(string[] args) + { + var baseUrl = SseHelper.Env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + var apiKey = SseHelper.Env("AXME_API_KEY"); + var toAgent = SseHelper.Env("AXME_TO_AGENT"); + if (string.IsNullOrEmpty(apiKey)) { Console.Error.WriteLine("AXME_API_KEY is required"); return; } + if (string.IsNullOrEmpty(toAgent)) { Console.Error.WriteLine("AXME_TO_AGENT is required"); return; } + + var client = new AxmeClient(new AxmeClientConfig { ApiKey = apiKey, BaseUrl = baseUrl }); + Console.WriteLine($"creating intent to {toAgent} ..."); + + var created = await client.CreateIntentAsync(new JsonObject + { + ["intent_type"] = "intent.compliance.check.v1", + ["from_agent"] = "initiator://simple-request", + ["to_agent"] = toAgent, + ["correlation_id"] = Guid.NewGuid().ToString(), + ["payload"] = new JsonObject { ["change_id"] = "CHG-MODEL-A-001", ["service"] = "api-gateway", + ["version"] = "4.0.0", ["environment"] = "staging", ["change_type"] = "config_update", ["risk_level"] = "low" } + }); + var intentId = created["intent_id"]?.ToString() ?? ""; + Console.WriteLine($"intent created: {intentId}\nwaiting for agent response..."); + + var since = 0; + var deadline = DateTime.UtcNow.AddSeconds(120); + while (DateTime.UtcNow < deadline) + { + var events = await client.ListIntentEventsAsync(intentId, since); + foreach (var evt in events["events"]?.AsArray() ?? []) + { + var status = evt?["status"]?.ToString() ?? ""; + if (evt?["seq"]?.GetValue() is int seq) since = Math.Max(since, seq); + if (status is "COMPLETED" or "IN_PROGRESS" or "FAILED" or "CANCELED" or "TIMED_OUT") + { + Console.WriteLine($"intent {intentId} → {status}"); + if (status is "COMPLETED" or "IN_PROGRESS") Console.WriteLine("SUCCESS"); + return; + } + } + await Task.Delay(1000); + } + Console.Error.WriteLine("timeout"); + } +} diff --git a/examples/model-a/simple-request/go/initiator.go b/examples/model-a/simple-request/go/initiator.go new file mode 100644 index 0000000..b6c9a4c --- /dev/null +++ b/examples/model-a/simple-request/go/initiator.go @@ -0,0 +1,84 @@ +// Model A — Simple Request: initiator waits for agent response (Go). +// +// Run: +// +// AXME_API_KEY= AXME_TO_AGENT=agent://org/ws/compliance-checker-agent \ +// go run examples/model-a/simple-request/initiator.go +package main + +import ( + "context" + "crypto/rand" + "fmt" + "log" + "os" + "time" + + "github.com/AxmeAI/axme-sdk-go/axme" +) + +func newUUID() string { + b := make([]byte, 16) + rand.Read(b) + return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) +} + +func main() { + apiKey := os.Getenv("AXME_API_KEY") + baseURL := envOr("AXME_BASE_URL", "https://api.cloud.axme.ai") + toAgent := os.Getenv("AXME_TO_AGENT") + + if apiKey == "" { log.Fatal("AXME_API_KEY is required") } + if toAgent == "" { log.Fatal("AXME_TO_AGENT is required") } + + client, err := axme.NewClient(axme.ClientConfig{APIKey: apiKey, BaseURL: baseURL}) + if err != nil { log.Fatalf("client init: %v", err) } + + ctx := context.Background() + + log.Printf("creating intent to %s ...", toAgent) + created, err := client.CreateIntent(ctx, map[string]any{ + "intent_type": "intent.compliance.check.v1", + "correlation_id": newUUID(), + "from_agent": "initiator://simple-request", + "to_agent": toAgent, + "payload": map[string]any{ + "change_id": "CHG-MODEL-A-001", "service": "api-gateway", "version": "4.0.0", + "environment": "staging", "change_type": "config_update", "risk_level": "low", + }, + }, axme.RequestOptions{}) + if err != nil { log.Fatalf("create_intent failed: %v", err) } + + intentID, _ := created["intent_id"].(string) + log.Printf("intent created: %s", intentID) + log.Println("waiting for agent response...") + + deadline := time.After(120 * time.Second) + since := 0 + sincePtr := &since + for { + select { + case <-deadline: + log.Fatal("timeout waiting for agent response") + default: + } + + events, err := client.ListIntentEvents(ctx, intentID, sincePtr, axme.RequestOptions{}) + if err != nil { time.Sleep(time.Second); continue } + + evtList, _ := events["events"].([]any) + for _, e := range evtList { + evt, _ := e.(map[string]any) + status, _ := evt["status"].(string) + if seq, ok := evt["seq"].(float64); ok { since = int(seq) } + if status == "COMPLETED" || status == "IN_PROGRESS" || status == "FAILED" || status == "CANCELED" || status == "TIMED_OUT" { + log.Printf("intent %s → %s", intentID, status) + if status == "COMPLETED" || status == "IN_PROGRESS" { log.Println("SUCCESS — agent processed the request") } + return + } + } + time.Sleep(time.Second) + } +} + +func envOr(k, d string) string { if v := os.Getenv(k); v != "" { return v }; return d } diff --git a/examples/model-a/simple-request/java/SimpleRequestInitiator.java b/examples/model-a/simple-request/java/SimpleRequestInitiator.java new file mode 100644 index 0000000..c1fbb91 --- /dev/null +++ b/examples/model-a/simple-request/java/SimpleRequestInitiator.java @@ -0,0 +1,48 @@ +package ai.axme.examples.modela; + +import ai.axme.examples.SseHelper; +import dev.axme.sdk.*; +import java.util.*; + +/** Model A — Simple Request: initiator waits for agent response (Java). */ +public class SimpleRequestInitiator { + public static void main(String[] args) throws Exception { + String apiKey = System.getenv("AXME_API_KEY"); + String baseUrl = SseHelper.env("AXME_BASE_URL", "https://api.cloud.axme.ai"); + String toAgent = System.getenv("AXME_TO_AGENT"); + if (apiKey == null || apiKey.isEmpty()) { System.err.println("AXME_API_KEY is required"); System.exit(1); } + if (toAgent == null || toAgent.isEmpty()) { System.err.println("AXME_TO_AGENT is required"); System.exit(1); } + + AxmeClient client = new AxmeClient(new AxmeClientConfig(baseUrl, apiKey)); + System.out.printf("creating intent to %s ...%n", toAgent); + + Map created = client.createIntent(Map.of( + "intent_type", "intent.compliance.check.v1", + "from_agent", "initiator://simple-request", + "to_agent", toAgent, + "correlation_id", UUID.randomUUID().toString(), + "payload", Map.of("change_id", "CHG-MODEL-A-001", "service", "api-gateway", "version", "4.0.0", + "environment", "staging", "change_type", "config_update", "risk_level", "low") + ), RequestOptions.none()); + String intentId = (String) created.get("intent_id"); + System.out.printf("intent created: %s%nwaiting for agent response...%n", intentId); + + int since = 0; + long deadline = System.currentTimeMillis() + 120_000; + while (System.currentTimeMillis() < deadline) { + Map events = client.listIntentEvents(intentId, since, RequestOptions.none()); + @SuppressWarnings("unchecked") List> evts = (List>) events.getOrDefault("events", List.of()); + for (Map evt : evts) { + String status = SseHelper.str(evt, "status"); + if (evt.get("seq") instanceof Number n) since = Math.max(since, n.intValue()); + if (Set.of("COMPLETED", "IN_PROGRESS", "FAILED", "CANCELED", "TIMED_OUT").contains(status)) { + System.out.printf("intent %s → %s%n", intentId, status); + if ("COMPLETED".equals(status) || "IN_PROGRESS".equals(status)) System.out.println("SUCCESS"); + return; + } + } + Thread.sleep(1000); + } + System.err.println("timeout"); + } +} diff --git a/examples/model-a/simple-request/initiator.py b/examples/model-a/simple-request/python/initiator.py similarity index 100% rename from examples/model-a/simple-request/initiator.py rename to examples/model-a/simple-request/python/initiator.py diff --git a/examples/model-a/simple-request/typescript/initiator.ts b/examples/model-a/simple-request/typescript/initiator.ts new file mode 100644 index 0000000..51c237e --- /dev/null +++ b/examples/model-a/simple-request/typescript/initiator.ts @@ -0,0 +1,46 @@ +/** + * Model A — Simple Request: initiator waits for agent response (TypeScript). + * + * Run: + * # Terminal 1 — start the agent + * AXME_API_KEY= npx tsx examples/delivery/stream/agent.ts + * + * # Terminal 2 — run the initiator + * AXME_API_KEY= AXME_TO_AGENT=agent://org/ws/compliance-checker-agent \ + * npx tsx examples/model-a/simple-request/initiator.ts + */ +import { AxmeClient } from "@axme/axme"; + +const AXME_BASE_URL = process.env.AXME_BASE_URL ?? "https://api.cloud.axme.ai"; +const AXME_API_KEY = process.env.AXME_API_KEY ?? ""; +const AXME_TO_AGENT = process.env.AXME_TO_AGENT ?? ""; + +if (!AXME_API_KEY) { console.error("AXME_API_KEY is required (workspace API key)"); process.exit(1); } +if (!AXME_TO_AGENT) { console.error("AXME_TO_AGENT is required (e.g. agent://org/workspace/compliance-checker-agent)"); process.exit(1); } + +const client = new AxmeClient({ apiKey: AXME_API_KEY, baseUrl: AXME_BASE_URL }); + +// 1. Create intent +console.log(`creating intent to ${AXME_TO_AGENT} ...`); +const intentId = await client.sendIntent({ + intent_type: "intent.compliance.check.v1", + from_agent: "initiator://simple-request", + to_agent: AXME_TO_AGENT, + payload: { + change_id: "CHG-MODEL-A-001", service: "api-gateway", version: "4.0.0", + environment: "staging", change_type: "config_update", risk_level: "low", + }, +}); +console.log(`intent created: ${intentId}`); + +// 2. Wait for agent to process +console.log("waiting for agent response..."); +for await (const event of client.observe(intentId, { timeoutMs: 120_000 })) { + const status = String(event.status ?? ""); + if (["COMPLETED", "IN_PROGRESS", "FAILED", "CANCELED", "TIMED_OUT"].includes(status)) { + console.log(`intent ${intentId} → ${status}`); + if (["COMPLETED", "IN_PROGRESS"].includes(status)) console.log("SUCCESS — agent processed the request"); + else console.warn(`intent ended with status: ${status}`); + break; + } +} diff --git a/lang/dotnet/AxmeExamples.csproj b/lang/dotnet/AxmeExamples.csproj new file mode 100644 index 0000000..d31a7c3 --- /dev/null +++ b/lang/dotnet/AxmeExamples.csproj @@ -0,0 +1,12 @@ + + + Exe + net8.0 + enable + enable + $(ExampleClass) + + + + + diff --git a/lang/dotnet/SseHelper.cs b/lang/dotnet/SseHelper.cs new file mode 100644 index 0000000..ccfed99 --- /dev/null +++ b/lang/dotnet/SseHelper.cs @@ -0,0 +1,73 @@ +using System.Net.Http.Headers; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace AxmeExamples; + +/// +/// Shared SSE helper for agent intent stream polling. +/// The .NET SDK does not include a built-in SSE/listen method. +/// +public static class SseHelper +{ + private static readonly HttpClient Http = new(); + + public static async Task> PollAgentStreamAsync( + string baseUrl, string apiKey, string agentAddress, + int since = 0, int waitSeconds = 15, CancellationToken ct = default) + { + var path = agentAddress.StartsWith("agent://") ? agentAddress["agent://".Length..] : agentAddress; + var url = $"{baseUrl}/v1/agents/{path}/intents/stream?since={since}&wait_seconds={waitSeconds}"; + + using var req = new HttpRequestMessage(HttpMethod.Get, url); + req.Headers.Add("x-api-key", apiKey); + req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream")); + + using var resp = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct); + resp.EnsureSuccessStatusCode(); + + var results = new List(); + using var reader = new StreamReader(await resp.Content.ReadAsStreamAsync(ct)); + + var eventType = ""; + var data = ""; + + while (await reader.ReadLineAsync(ct) is { } line) + { + if (line.StartsWith("event: ")) eventType = line[7..].Trim(); + else if (line.StartsWith("data: ")) data += line[6..]; + else if (line.Length == 0 && data.Length > 0) + { + if (eventType.StartsWith("intent.")) + { + var obj = JsonNode.Parse(data)?.AsObject(); + if (obj != null) results.Add(obj); + } + eventType = ""; + data = ""; + } + } + return results; + } + + public static string Env(string key, string def = "") => + Environment.GetEnvironmentVariable(key) is { Length: > 0 } v ? v : def; + + public static string Str(JsonObject obj, string key, string def = "") => + obj.TryGetPropertyValue(key, out var v) && v is JsonValue jv ? jv.ToString() : def; + + public static JsonObject EffectivePayload(JsonObject intent) + { + var raw = intent["payload"]?.AsObject() ?? new JsonObject(); + return raw["parent_payload"]?.AsObject() ?? raw; + } + + public static JsonObject UnwrapIntent(JsonObject resp) => + resp["intent"]?.AsObject() ?? resp; + + public static bool IsActionable(JsonObject intent) + { + var status = (Str(intent, "lifecycle_status") is { Length: > 0 } s ? s : Str(intent, "status")).ToUpper(); + return status is "CREATED" or "DELIVERED" or "ACKNOWLEDGED" or "IN_PROGRESS" or "WAITING"; + } +} diff --git a/lang/go/go.mod b/lang/go/go.mod new file mode 100644 index 0000000..b97527e --- /dev/null +++ b/lang/go/go.mod @@ -0,0 +1,7 @@ +module github.com/AxmeAI/axme-examples + +go 1.22 + +require github.com/AxmeAI/axme-sdk-go v0.1.1 + +replace github.com/AxmeAI/axme-sdk-go => ../axme-sdk-go diff --git a/lang/java/SseHelper.java b/lang/java/SseHelper.java new file mode 100644 index 0000000..b1ce745 --- /dev/null +++ b/lang/java/SseHelper.java @@ -0,0 +1,115 @@ +package ai.axme.examples; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.*; + +/** + * Shared SSE helper for agent intent stream polling. + * The Java SDK does not include a built-in SSE/listen method, + * so this helper provides a minimal SSE client for the agent inbox stream. + */ +public final class SseHelper { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private SseHelper() {} + + /** + * Poll the agent intent stream (SSE endpoint). + * Returns parsed intent deliveries from the stream. + */ + public static List> pollAgentStream( + String baseUrl, String apiKey, String agentAddress, + int since, int waitSeconds) throws Exception { + + String path = agentAddress.startsWith("agent://") + ? agentAddress.substring("agent://".length()) : agentAddress; + String url = baseUrl + "/v1/agents/" + path + "/intents/stream" + + "?since=" + since + "&wait_seconds=" + waitSeconds; + + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); + conn.setRequestProperty("x-api-key", apiKey); + conn.setRequestProperty("Accept", "text/event-stream"); + conn.setConnectTimeout(5000); + conn.setReadTimeout((waitSeconds + 5) * 1000); + + List> results = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(conn.getInputStream()))) { + String eventType = ""; + StringBuilder data = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("event: ")) { + eventType = line.substring(7).trim(); + } else if (line.startsWith("data: ")) { + data.append(line.substring(6)); + } else if (line.isEmpty() && data.length() > 0) { + if (eventType.startsWith("intent.")) { + results.add(parseJson(data.toString())); + } + eventType = ""; + data.setLength(0); + } + } + } + return results; + } + + @SuppressWarnings("unchecked") + public static Map parseJson(String json) { + try { + return MAPPER.readValue(json, Map.class); + } catch (Exception e) { + return Map.of(); + } + } + + public static String env(String key, String def) { + String v = System.getenv(key); + return v != null && !v.isEmpty() ? v : def; + } + + public static String str(Map m, String key) { + return str(m, key, ""); + } + + public static String str(Map m, String key, String def) { + Object v = m.get(key); + return v instanceof String s && !s.isEmpty() ? s : def; + } + + /** Extract effective payload (handles step-intent wrapping). */ + @SuppressWarnings("unchecked") + public static Map effectivePayload(Map intent) { + Map raw = intent.containsKey("payload") + ? (Map) intent.get("payload") : Map.of(); + if (raw == null) raw = Map.of(); + Object pp = raw.get("parent_payload"); + return pp instanceof Map ? (Map) pp : raw; + } + + /** Unwrap intent from response. */ + @SuppressWarnings("unchecked") + public static Map unwrapIntent(Map resp) { + Object i = resp.get("intent"); + return i instanceof Map ? (Map) i : resp; + } + + /** Check if intent status is actionable. */ + public static boolean isActionable(Map intent) { + String status = str(intent, "lifecycle_status"); + if (status.isEmpty()) status = str(intent, "status"); + return Set.of("CREATED", "DELIVERED", "ACKNOWLEDGED", "IN_PROGRESS", "WAITING") + .contains(status.toUpperCase()); + } + + /** Create RequestOptions with owner_agent set. */ + public static dev.axme.sdk.RequestOptions withOwner(String ownerAgent) { + return new dev.axme.sdk.RequestOptions(null, null, ownerAgent, null, null); + } +} diff --git a/lang/java/pom.xml b/lang/java/pom.xml new file mode 100644 index 0000000..9048803 --- /dev/null +++ b/lang/java/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + ai.axme + axme-examples-java + 0.1.0 + + 17 + 17 + UTF-8 + + + + ai.axme + axme + 0.1.1 + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + + diff --git a/lang/typescript/package.json b/lang/typescript/package.json new file mode 100644 index 0000000..3c7ff88 --- /dev/null +++ b/lang/typescript/package.json @@ -0,0 +1,17 @@ +{ + "name": "axme-examples", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "AXME platform examples in TypeScript, Go, Java, and .NET", + "scripts": { + "ts": "tsx" + }, + "dependencies": { + "@axme/axme": "file:../axme-sdk-typescript" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.20.0" + } +} diff --git a/lang/typescript/tsconfig.json b/lang/typescript/tsconfig.json new file mode 100644 index 0000000..0721734 --- /dev/null +++ b/lang/typescript/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist" + }, + "include": ["examples/**/*.ts"] +} diff --git a/snippets/README.md b/snippets/README.md deleted file mode 100644 index 53d5ed1..0000000 --- a/snippets/README.md +++ /dev/null @@ -1,86 +0,0 @@ -# Additional SDK Snippets - -This directory tracks short usage snippets for SDKs that are not maintained as full runnable projects in every scenario. - -Current policy: - -- Full runnable examples: **Python** and **TypeScript** -- Snippet-only support: **Go**, **Java**, **.NET** -- Protocol-only examples (no cloud runtime): `../protocol/` - -These snippets are shown for AXME Cloud usage and require an API key generated on the landing page. -Get API key: - -Environment model: - -- `AXME_API_KEY` - required -- `AXME_BASE_URL` - optional override - -## Auto-Approval Workflow Snippet - -### Go - -```go -resp, _ := client.CreateIntent(ctx, axme.CreateIntentRequest{ - IntentType: "intent.approval.demo.v1", - CorrelationID: correlationID, - FromAgent: "agent://requester", - ToAgent: "agent://approver", - Payload: map[string]any{"request_id": "req-123"}, -}) -_ = client.ResumeIntent(ctx, resp.IntentID, axme.ResumeIntentRequest{ - ApproveCurrentStep: true, - Reason: "auto-approved by policy", -}, axme.WithOwnerAgent("agent://requester")) -_ = client.ResolveIntent(ctx, resp.IntentID, axme.ResolveIntentRequest{ - Status: "COMPLETED", - Result: map[string]any{"approved": true, "mode": "automatic"}, -}) -``` - -### Java - -```java -var created = client.createIntent(Map.of( - "intent_type", "intent.approval.demo.v1", - "correlation_id", correlationId, - "from_agent", "agent://requester", - "to_agent", "agent://approver", - "payload", Map.of("request_id", "req-123") -), CreateIntentOptions.builder().correlationId(correlationId).build()); - -client.resumeIntent(created.intentId(), Map.of( - "approve_current_step", true, - "reason", "auto-approved by policy" -), ResolveIntentOptions.builder().ownerAgent("agent://requester").build()); - -client.resolveIntent(created.intentId(), Map.of( - "status", "COMPLETED", - "result", Map.of("approved", true, "mode", "automatic") -)); -``` - -### .NET - -```csharp -var created = await client.CreateIntentAsync(new Dictionary -{ - ["intent_type"] = "intent.approval.demo.v1", - ["correlation_id"] = correlationId, - ["from_agent"] = "agent://requester", - ["to_agent"] = "agent://approver", - ["payload"] = new Dictionary { ["request_id"] = "req-123" }, -}, new CreateIntentOptions { CorrelationId = correlationId }); - -await client.ResumeIntentAsync(created.IntentId, new Dictionary -{ - ["approve_current_step"] = true, - ["reason"] = "auto-approved by policy", -}, new ResolveIntentOptions { OwnerAgent = "agent://requester" }); - -await client.ResolveIntentAsync(created.IntentId, new Dictionary -{ - ["status"] = "COMPLETED", - ["result"] = new Dictionary { ["approved"] = true, ["mode"] = "automatic" }, -}); -```