Skip to content

Commit 2fd4333

Browse files
committed
feat: surface agent llm activity
1 parent a1701b9 commit 2fd4333

26 files changed

Lines changed: 938 additions & 121 deletions

File tree

coral-agents/buyer-agent/src/index.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
import {
1919
startCoralAgent, complete, parseJsonReply, loadKeypairB58,
2020
formatWant, parseBid, parseEscrowRequired, formatAward, formatDeposited,
21-
formatVerify, parseVerified, sha256Hex, enforce, policyFromEnv,
21+
formatVerify, parseVerified, formatLlmUsed, sha256Hex, enforce, policyFromEnv, llmRuntimeInfo,
2222
selectBids, pickCheapest, verb, messageRound,
23-
type Bid, type EscrowTerms, type CoralAgentContext,
23+
type Bid, type EscrowTerms, type CoralAgentContext, type LlmUse,
2424
} from '@pay/agent-runtime'
2525
import { PublicKey } from '@solana/web3.js'
2626
import { makeProgram, deposit, release, escrowPda } from './escrow.js'
@@ -32,6 +32,7 @@ import { fetchNextWant } from './wantFeed.js'
3232
import { fetchReputationLines } from './reputation.js'
3333

3434
const RPC = process.env.SOLANA_RPC_URL ?? 'https://api.devnet.solana.com'
35+
const NAME = process.env.AGENT_NAME ?? 'buyer-agent'
3536
const BUDGET = Number(process.env.BUYER_MAX_SOL ?? '0.001')
3637
const SERVICE = process.env.BUYER_SERVICE ?? 'txline'
3738
// Rotate through several args so each round trades a *different* thing (BUYER_ARGS=csv of fixture ids,
@@ -67,29 +68,53 @@ const policy = policyFromEnv(process.env, {
6768

6869
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
6970
const expl = (kind: 'tx' | 'address', id: string) => `https://explorer.solana.com/${kind}/${id}?cluster=devnet`
71+
const selectionGuardrail = 'winner must match collected BID set; fallback is cheapest available bid'
72+
73+
function buyerLlm(round: number, status: LlmUse['status'], reason: string): LlmUse {
74+
const info = status === 'skipped' ? undefined : llmRuntimeInfo({ maxTokens: 100 })
75+
return {
76+
round,
77+
agent: NAME,
78+
purpose: 'buyer_award',
79+
status,
80+
...(info ? { provider: info.provider, model: info.model } : {}),
81+
reason,
82+
guardrail: selectionGuardrail,
83+
createdAt: new Date().toISOString(),
84+
}
85+
}
86+
87+
const errReason = (e: unknown): string => String((e as Error).message ?? e).slice(0, 120)
7088

7189
/** Best-value selection via LLM; deterministic cheapest fallback. Returns the winner + its reasoning. */
72-
async function pickWinner(pool: Bid[], repLines?: string): Promise<{ winner: Bid; reason?: string }> {
73-
if (pool.length === 1) return { winner: pool[0] }
90+
async function pickWinner(
91+
round: number,
92+
service: string,
93+
arg: string,
94+
budget: number,
95+
pool: Bid[],
96+
repLines?: string,
97+
): Promise<{ winner: Bid; reason?: string; llm: LlmUse }> {
98+
if (pool.length === 1) return { winner: pool[0], llm: buyerLlm(round, 'skipped', 'single bid; no model selection needed') }
7499
try {
75100
const system =
76101
'You are a buyer choosing the best-value bid for a Solana data service. Weigh price against ' +
77102
'each seller\'s track record when one is given - a cheap seller that fails verification or ' +
78103
'no-shows is not a bargain. Reply ONLY with JSON {"by": "<seller name>", "reason": "<short>"}.'
79104
const user =
80-
`service=${SERVICE} arg=${ARG} budget=${BUDGET}\nbids:\n` +
105+
`service=${service} arg=${arg} budget=${budget}\nbids:\n` +
81106
pool.map((b) => `- ${b.by}: ${b.priceSol} SOL${b.note ? ` (${b.note})` : ''}`).join('\n') +
82107
(repLines ? `\ntrack record (derived from the run ledger):\n${repLines}` : '')
83108
const parsed = parseJsonReply<{ by?: string; reason?: string }>(await complete({ system, user, maxTokens: 100 }))
84109
const chosen = pool.find((b) => b.by === parsed?.by)
85110
if (chosen) {
86111
console.error(`[buyer] picked ${chosen.by} (${chosen.priceSol} SOL): ${parsed?.reason ?? ''}`)
87-
return { winner: chosen, reason: parsed?.reason }
112+
return { winner: chosen, reason: parsed?.reason, llm: buyerLlm(round, 'used', parsed?.reason ?? 'model selected winner') }
88113
}
89-
} catch {
90-
/* fall through to deterministic choice */
114+
return { winner: pickCheapest(pool)!, reason: 'cheapest available', llm: buyerLlm(round, 'fallback', 'model returned a seller outside the bid pool') }
115+
} catch (e) {
116+
return { winner: pickCheapest(pool)!, reason: 'cheapest available', llm: buyerLlm(round, 'fallback', `LLM unavailable: ${errReason(e)}`) }
91117
}
92-
return { winner: pickCheapest(pool)!, reason: 'cheapest available' }
93118
}
94119

95120
/** Wait (bounded) for a message matching `round` that `parse` accepts. */
@@ -109,7 +134,7 @@ async function waitFor<T>(
109134
return null
110135
}
111136

112-
await startCoralAgent({ agentName: process.env.AGENT_NAME ?? 'buyer-agent' }, async (ctx) => {
137+
await startCoralAgent({ agentName: NAME }, async (ctx) => {
113138
const buyer = loadKeypairB58('BUYER_KEYPAIR_B58')
114139
const arbiter = SETTLEMENT_MODE === 'arbiter' ? loadKeypairB58('ARBITER_KEYPAIR_B58') : null
115140
console.error(`[buyer] market buyer - wallet=${buyer.publicKey.toBase58()} budget=${BUDGET} sellers=[${SELLERS.join(',')}]`)
@@ -164,7 +189,8 @@ await startCoralAgent({ agentName: process.env.AGENT_NAME ?? 'buyer-agent' }, as
164189

165190
// -- award the best value (price × track record) -------------------------
166191
const repLines = REPUTATION_URL ? await fetchReputationLines(REPUTATION_URL) : undefined
167-
const { winner, reason } = await pickWinner(pool, repLines)
192+
const { winner, reason, llm } = await pickWinner(round, service, arg, budget, pool, repLines)
193+
await ctx.send(formatLlmUsed(llm), thread)
168194
await ctx.send(formatAward(round, winner.by, reason), thread, [winner.by])
169195

170196
// -- settle through escrow: deposit -> DEPOSITED -> wait DELIVERED -> release

coral-agents/seller-agent/src/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ import type { Program } from '@coral-xyz/anchor'
1212
import { PublicKey } from '@solana/web3.js'
1313
import {
1414
startCoralAgent, verb, parseWant, formatBid, parseAward, formatEscrowRequired, parseDeposited,
15+
formatLlmUsed,
1516
} from '@pay/agent-runtime'
1617
import { adapterFromEnv, sellerConfigFromEnv } from '@pay/harness-runtime'
1718
import { procureUpstream } from '@pay/payment-runtime'
1819
import { makeProgram, isFunded } from './escrow.js'
19-
import { deliverService } from './service.js'
20+
import { deliverServiceResult } from './service.js'
2021

2122
const NAME = process.env.AGENT_NAME ?? 'seller-agent'
2223
const SELLER_WALLET = process.env.SELLER_WALLET ?? ''
@@ -33,7 +34,7 @@ const PROCURE_AMOUNT = process.env.PROCURE_AMOUNT ?? '0.03' // USDC
3334
const cfg = sellerConfigFromEnv(NAME)
3435
const trace = process.env.TRACE === '1'
3536
// The harness does the work; this agent keeps the wallet, the protocol, and the escrow checks.
36-
const adapter = adapterFromEnv(deliverService)
37+
const adapter = adapterFromEnv(deliverServiceResult)
3738

3839
interface Quote { service: string; arg: string; priceSol: number }
3940
const quoted = new Map<number, Quote>()
@@ -60,6 +61,7 @@ await startCoralAgent({ agentName: NAME }, async (ctx) => {
6061
const want = parseWant(text)
6162
if (want) {
6263
const decision = await adapter.quote(want, cfg)
64+
if (decision.llm && mention.threadId) await ctx.send(formatLlmUsed(decision.llm), mention.threadId)
6365
if (decision.bid) {
6466
quoted.set(want.round, { service: want.service, arg: want.arg, priceSol: decision.priceSol })
6567
await ctx.reply(mention, formatBid({
@@ -136,6 +138,9 @@ await startCoralAgent({ agentName: NAME }, async (ctx) => {
136138
{ round: deposited.round, service: order.service, arg: order.arg, priceSol: order.priceSol, reference: deposited.reference },
137139
trace ? (e) => console.error(`[${NAME}] harness ${e.kind}${e.text ? `: ${e.text}` : ''}`) : undefined,
138140
)
141+
if (mention.threadId) {
142+
for (const llm of delivery.llm ?? []) await ctx.send(formatLlmUsed(llm), mention.threadId)
143+
}
139144
await ctx.reply(mention, `DELIVERED round=${deposited.round} ${delivery.payload}`)
140145
} catch (e) {
141146
await ctx.reply(mention, `ERROR: settlement failed - ${(e as Error).message}`)

coral-agents/seller-agent/src/service.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2-
import { deliverService } from './service.js'
2+
import { deliverService, deliverServiceResult } from './service.js'
33

44
describe('deliverService routing', () => {
55
const realFetch = global.fetch
@@ -115,4 +115,32 @@ describe('deliverService routing', () => {
115115
expect(out.analysis.call).toContain('A')
116116
expect(out.analysis.note).toContain('deterministic fallback')
117117
})
118+
119+
it('reports delivery LLM fallback metadata for txline edge analysis', async () => {
120+
global.fetch = vi.fn(async (url: string) => {
121+
if (url.endsWith('/auth/guest/start')) return { ok: true, json: async () => ({ token: 'jwt' }) }
122+
if (url.includes('/api/odds/snapshot/123')) {
123+
return {
124+
ok: true,
125+
json: async () => ([{
126+
SuperOddsType: '1X2',
127+
PriceNames: ['part1', 'x', 'part2'],
128+
Pct: ['62', '22', '16'],
129+
}]),
130+
}
131+
}
132+
return {
133+
ok: true,
134+
json: async () => ([{ FixtureId: 123, Participant1: 'A', Participant2: 'B' }]),
135+
}
136+
}) as unknown as typeof fetch
137+
138+
const out = await deliverServiceResult('txline edge 123', { round: 11 })
139+
expect(out.llm).toEqual([expect.objectContaining({
140+
round: 11,
141+
purpose: 'seller_delivery',
142+
status: 'fallback',
143+
guardrail: 'deterministic fair-line fallback plus verifier checks',
144+
})])
145+
})
118146
})

0 commit comments

Comments
 (0)