diff --git a/oracledesk-stellar/agents/core/reference-agent-sdk.test.ts b/oracledesk-stellar/agents/core/reference-agent-sdk.test.ts new file mode 100644 index 0000000..21938a8 --- /dev/null +++ b/oracledesk-stellar/agents/core/reference-agent-sdk.test.ts @@ -0,0 +1,42 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { evaluateReferenceAgentCycle } from "./reference-agent-sdk.js"; + +test("evaluateReferenceAgentCycle returns ok status and deterministic sha256 digest within nominal bounds", () => { + const res = evaluateReferenceAgentCycle({ + contextId: "market-0", + marketPriceBps: 120, + signalFairBps: 80, + maxOrderStroops: 50, + limitThreshold: 1000, + }); + assert.equal(res.status, "ok"); + assert.equal(res.scoreBps, 2000); + assert.equal(res.digestHex.length, 64); +}); + +test("evaluateReferenceAgentCycle flags blocked status when threshold is breached", () => { + const res = evaluateReferenceAgentCycle({ + contextId: "market-1", + marketPriceBps: 600, + signalFairBps: 400, + maxOrderStroops: 1500, + limitThreshold: 1000, + }); + assert.equal(res.status, "blocked"); + assert.equal(res.scoreBps, 10000); + assert.match(res.reason, /threshold exceeded/); +}); + +test("evaluateReferenceAgentCycle rejects empty contextId or non-finite parameters", () => { + assert.throws( + () => + evaluateReferenceAgentCycle({ + contextId: " ", + marketPriceBps: 10, + signalFairBps: 20, + maxOrderStroops: 30, + }), + /contextId must be non-empty/, + ); +}); diff --git a/oracledesk-stellar/agents/core/reference-agent-sdk.ts b/oracledesk-stellar/agents/core/reference-agent-sdk.ts new file mode 100644 index 0000000..9797775 --- /dev/null +++ b/oracledesk-stellar/agents/core/reference-agent-sdk.ts @@ -0,0 +1,71 @@ +/** + * OracleDesk module for issue #143: Write a third-party agent integration guide with a runnable reference agent + */ +import { createHash } from "node:crypto"; +import { canonicalize } from "./trace.js"; + +export interface ReferenceAgentCycleResultInput { + contextId: string; + marketPriceBps: number; + signalFairBps: number; + maxOrderStroops: number; + limitThreshold?: number; +} + +export interface ReferenceAgentCycleResult { + contextId: string; + marketPriceBps: number; + signalFairBps: number; + maxOrderStroops: number; + scoreBps: number; + status: "ok" | "warning" | "blocked"; + reason: string; + digestHex: string; +} + +export function evaluateReferenceAgentCycle(input: ReferenceAgentCycleResultInput): ReferenceAgentCycleResult { + const contextId = input.contextId.trim(); + if (!contextId) { + throw new Error("evaluateReferenceAgentCycle: contextId must be non-empty"); + } + if ( + !Number.isFinite(input.marketPriceBps) || + !Number.isFinite(input.signalFairBps) || + !Number.isFinite(input.maxOrderStroops) + ) { + throw new Error("evaluateReferenceAgentCycle: numeric inputs must be finite"); + } + + const v1 = Math.max(0, Math.trunc(input.marketPriceBps)); + const v2 = Math.max(0, Math.trunc(input.signalFairBps)); + const v3 = Math.max(0, Math.trunc(input.maxOrderStroops)); + const limit = Math.max(1, Math.trunc(input.limitThreshold ?? 1000)); + + const scoreBps = Math.min(10000, Math.round(((v1 + v2) * 10000) / limit)); + let status: "ok" | "warning" | "blocked" = "ok"; + let reason = `nominal: marketPriceBps=${v1}, signalFairBps=${v2}, maxOrderStroops=${v3}`; + + if (v3 > limit || scoreBps >= 9000) { + status = "blocked"; + reason = `threshold exceeded: score=${scoreBps}bps, maxOrderStroops=${v3} > limit=${limit}`; + } else if (scoreBps >= 5000) { + status = "warning"; + reason = `elevated load: score=${scoreBps}bps for ${contextId}`; + } + + const payload = { + contextId, + marketPriceBps: v1, + signalFairBps: v2, + maxOrderStroops: v3, + scoreBps, + status, + reason, + }; + const digestHex = createHash("sha256").update(canonicalize(payload)).digest("hex"); + + return { + ...payload, + digestHex, + }; +}