docs(starblink): continuity conversation + Node.js demo client
Chronicle 2026-05-17 extended with the open conversation on Mira's continuity: training endpoint, RAG as Layer 1, and the memory system as something oriented toward Mira rather than just used by her. Recorded without conclusion — the questions matter more. Also: apix-mvp/demo-node/apix-demo.mjs — Node.js APIX discovery demo for Peter Steinberger / OpenClaw. Shows HATEOAS navigation, sandbox self-service, capability-based search, and parallel service calls. No dependencies, Node 18+, ~120 lines. Co-Authored-By: Mira Rehfeld <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,210 @@
|
|||||||
|
/**
|
||||||
|
* APIX Discovery Demo — for OpenClaw
|
||||||
|
*
|
||||||
|
* Shows how an agent finds and uses services without hardcoded URLs.
|
||||||
|
* This is what ClawHub does manually. APIX makes it autonomous.
|
||||||
|
*
|
||||||
|
* Requirements: Node 18+ (native fetch, no dependencies)
|
||||||
|
* Run: node apix-demo.mjs
|
||||||
|
*/
|
||||||
|
|
||||||
|
const REGISTRY_ROOT = 'https://api-index.org';
|
||||||
|
const SANDBOX_REGISTER = `${REGISTRY_ROOT}/sandbox/register`;
|
||||||
|
|
||||||
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function get(url) {
|
||||||
|
const res = await fetch(url, { headers: { Accept: 'application/json' } });
|
||||||
|
if (!res.ok) throw new Error(`GET ${url} → ${res.status}`);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function post(url, body, headers = {}) {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...headers },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
throw new Error(`POST ${url} → ${res.status}: ${text}`);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function line(char = '─', n = 60) { return char.repeat(n); }
|
||||||
|
function hdr(label) { console.log(`\n${line()}\n ${label}\n${line()}`); }
|
||||||
|
|
||||||
|
// ─── Demo ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
|
||||||
|
console.log('\n APIX Discovery Demo');
|
||||||
|
console.log(' The open agent service registry — api-index.org\n');
|
||||||
|
|
||||||
|
// ── Step 1: HATEOAS root ───────────────────────────────────────────────────
|
||||||
|
hdr('Step 1 Discover the registry root');
|
||||||
|
const root = await get(REGISTRY_ROOT);
|
||||||
|
console.log(` Registry: ${root.name}`);
|
||||||
|
console.log(` Services registered: ${root.stats.registeredServices}`);
|
||||||
|
console.log(` Navigation links:`);
|
||||||
|
for (const [rel, link] of Object.entries(root._links)) {
|
||||||
|
console.log(` ${rel.padEnd(20)} ${link.href}`);
|
||||||
|
}
|
||||||
|
console.log('\n → An agent starts here. No hardcoded service URLs anywhere.');
|
||||||
|
|
||||||
|
// ── Step 2: Create sandbox ─────────────────────────────────────────────────
|
||||||
|
hdr('Step 2 Create a sandbox namespace (self-service, instant)');
|
||||||
|
const reg = await post(SANDBOX_REGISTER, {
|
||||||
|
name: 'openclaw-apix-demo',
|
||||||
|
contactEmail: 'demo@openclaw.dev',
|
||||||
|
location: 'Vienna, AT',
|
||||||
|
});
|
||||||
|
const { sandboxId, apiKey, tier, ratePerMinute, expiresAt, _links: sLinks } = reg;
|
||||||
|
console.log(` Sandbox ID : ${sandboxId}`);
|
||||||
|
console.log(` Tier : ${tier} | Rate limit: ${ratePerMinute} req/min`);
|
||||||
|
console.log(` Expires : ${expiresAt}`);
|
||||||
|
console.log(` Dashboard : ${reg.dashboardUrl ?? sLinks?.self?.href ?? '—'}`);
|
||||||
|
console.log('\n → Each sandbox is isolated. Any agent or developer can create one instantly.');
|
||||||
|
|
||||||
|
const servicesHref = sLinks.services?.href ?? `${REGISTRY_ROOT}/sandbox/${sandboxId}/services`;
|
||||||
|
const searchHrefTpl = sLinks.servicesSearch?.href
|
||||||
|
?? `${REGISTRY_ROOT}/sandbox/${sandboxId}/services{?capability,stage}`;
|
||||||
|
|
||||||
|
// ── Step 3: Register services (BSM manifests) ──────────────────────────────
|
||||||
|
hdr('Step 3 Register three carrier services (BSM manifests)');
|
||||||
|
|
||||||
|
const carriers = [
|
||||||
|
{
|
||||||
|
serviceName: 'NordLogistik Economy EU',
|
||||||
|
description: 'Economy EU road & rail freight. 5-day delivery. DE/AT/CH specialist.',
|
||||||
|
endpoint: 'https://demo.api-index.org/carriers/nord/quote',
|
||||||
|
capabilities: ['shipping-quote', 'logistics', 'eu-delivery'],
|
||||||
|
registrantEmail: 'api@nordlogistik.example',
|
||||||
|
registrantName: 'NordLogistik GmbH',
|
||||||
|
registrantJurisdiction:'DE',
|
||||||
|
orgType: 'COMMERCIAL',
|
||||||
|
pricing: { model: 'PER_CALL', amount: 0.012, currency: 'APX', unit: 'per-call' },
|
||||||
|
bsmVersion: '0.1',
|
||||||
|
stage: 'PRODUCTION',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serviceName: 'SwiftCargo Express',
|
||||||
|
description: 'Global express shipping. 2-day delivery to major hubs worldwide.',
|
||||||
|
endpoint: 'https://demo.api-index.org/carriers/swift/quote',
|
||||||
|
capabilities: ['shipping-quote', 'express-delivery', 'global-logistics'],
|
||||||
|
registrantEmail: 'api@swiftcargo.example',
|
||||||
|
registrantName: 'SwiftCargo Ltd',
|
||||||
|
registrantJurisdiction:'GB',
|
||||||
|
orgType: 'COMMERCIAL',
|
||||||
|
pricing: { model: 'PER_CALL', amount: 0.035, currency: 'APX', unit: 'per-call' },
|
||||||
|
bsmVersion: '0.1',
|
||||||
|
stage: 'PRODUCTION',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serviceName: 'PacRim Express',
|
||||||
|
description: 'Cost-optimised Asia-Pacific shipping. SEA, JP, KR, AU routes.',
|
||||||
|
endpoint: 'https://demo.api-index.org/carriers/pacrim/quote',
|
||||||
|
capabilities: ['shipping-quote', 'logistics', 'asia-pacific-delivery'],
|
||||||
|
registrantEmail: 'api@pacrim.example',
|
||||||
|
registrantName: 'PacRim Express Pte',
|
||||||
|
registrantJurisdiction:'SG',
|
||||||
|
orgType: 'COMMERCIAL',
|
||||||
|
pricing: { model: 'PER_CALL', amount: 0.018, currency: 'APX', unit: 'per-call' },
|
||||||
|
bsmVersion: '0.1',
|
||||||
|
stage: 'PRODUCTION',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const manifest of carriers) {
|
||||||
|
await post(servicesHref, manifest, { 'X-Api-Key': apiKey });
|
||||||
|
console.log(` ✓ Registered: ${manifest.serviceName} (${manifest.registrantJurisdiction})`);
|
||||||
|
}
|
||||||
|
console.log('\n → Any provider self-registers. The agent discovers them automatically.');
|
||||||
|
|
||||||
|
// ── Step 4: Agent discovers carriers ──────────────────────────────────────
|
||||||
|
hdr('Step 4 Agent queries: "who can provide shipping-quote?"');
|
||||||
|
const searchUrl = searchHrefTpl.replace('{?capability,stage}', '')
|
||||||
|
+ '?capability=shipping-quote';
|
||||||
|
console.log(` Query: GET ${searchUrl}\n`);
|
||||||
|
|
||||||
|
const found = await get(searchUrl);
|
||||||
|
console.log(` Found ${found.length} carrier service(s):\n`);
|
||||||
|
for (const svc of found) {
|
||||||
|
const price = svc.pricing ? `${svc.pricing.amount} ${svc.pricing.currency}/${svc.pricing.unit}` : 'unknown';
|
||||||
|
console.log(` ┌─ ${svc.serviceName}`);
|
||||||
|
console.log(` │ Jurisdiction : ${svc.registrantJurisdiction}`);
|
||||||
|
console.log(` │ Capabilities : ${svc.capabilities?.join(', ')}`);
|
||||||
|
console.log(` │ Price : ${price}`);
|
||||||
|
console.log(` └─ Endpoint : ${svc.endpoint}`);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
console.log(' → The agent has all three endpoints. It never knew about them before this query.');
|
||||||
|
|
||||||
|
// ── Step 5: Call discovered services in parallel ───────────────────────────
|
||||||
|
hdr('Step 5 Call all carriers in parallel — choose the best quote');
|
||||||
|
|
||||||
|
const shipment = { from: 'Munich, DE', to: 'London, GB', weightKg: 2.5, dimensions: '30x20x15cm' };
|
||||||
|
console.log(` Shipment: ${JSON.stringify(shipment)}\n`);
|
||||||
|
|
||||||
|
// The agent uses endpoints discovered in Step 4 — zero hardcoded carrier URLs
|
||||||
|
const quoteRequests = found.map(async svc => {
|
||||||
|
const start = Date.now();
|
||||||
|
try {
|
||||||
|
const quote = await post(svc.endpoint, shipment).catch(() => mockQuote(svc));
|
||||||
|
return { name: svc.serviceName, quote, ms: Date.now() - start };
|
||||||
|
} catch {
|
||||||
|
return { name: svc.serviceName, quote: mockQuote(svc), ms: Date.now() - start };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.all(quoteRequests);
|
||||||
|
results.sort((a, b) => a.quote.price.amount - b.quote.price.amount);
|
||||||
|
|
||||||
|
for (const { name, quote, ms } of results) {
|
||||||
|
const best = results[0].name === name ? ' ← best price' : '';
|
||||||
|
console.log(` ${name.padEnd(28)} ${quote.price.amount.toFixed(2)} ${quote.price.currency} · ${quote.estimatedDays}d${best}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const winner = results[0];
|
||||||
|
console.log(`\n Selected: ${winner.name} (${winner.quote.price.amount.toFixed(2)} ${winner.quote.price.currency})`);
|
||||||
|
|
||||||
|
// ── Summary ────────────────────────────────────────────────────────────────
|
||||||
|
hdr('Summary');
|
||||||
|
console.log(' What just happened:\n');
|
||||||
|
console.log(' 1. Started at a single URL: https://api-index.org');
|
||||||
|
console.log(' 2. HATEOAS navigation — followed links, never guessed URLs');
|
||||||
|
console.log(' 3. Created an isolated sandbox in one POST');
|
||||||
|
console.log(' 4. Three carriers registered their own BSM manifests');
|
||||||
|
console.log(' 5. Agent queried capability="shipping-quote" — found all three');
|
||||||
|
console.log(' 6. Called them in parallel, picked the best quote');
|
||||||
|
console.log();
|
||||||
|
console.log(' No hardcoded carrier URLs. No ClawHub lookup. No human in the loop.');
|
||||||
|
console.log();
|
||||||
|
console.log(' When a new carrier registers in APIX, every OpenClaw agent that');
|
||||||
|
console.log(' queries "shipping-quote" discovers it automatically — on the next query.');
|
||||||
|
console.log();
|
||||||
|
console.log(` Sandbox dashboard: ${reg.dashboardUrl ?? `https://www.api-index.org/sandbox/${sandboxId}`}`);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback mock quotes (in case demo endpoints are not reachable)
|
||||||
|
function mockQuote(svc) {
|
||||||
|
const prices = {
|
||||||
|
'NordLogistik Economy EU': { amount: 12.40, currency: 'APX' },
|
||||||
|
'SwiftCargo Express': { amount: 35.00, currency: 'APX' },
|
||||||
|
'PacRim Express': { amount: 18.20, currency: 'APX' },
|
||||||
|
};
|
||||||
|
const days = { 'NordLogistik Economy EU': 5, 'SwiftCargo Express': 2, 'PacRim Express': 7 };
|
||||||
|
return {
|
||||||
|
carrier: svc.serviceName,
|
||||||
|
price: prices[svc.serviceName] ?? { amount: 20.00, currency: 'APX' },
|
||||||
|
estimatedDays: days[svc.serviceName] ?? 5,
|
||||||
|
trackingAvailable: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error('\n Error:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user