@bnbagent/studio-cli 0.0.10 → 0.0.11-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +2 -2
- package/dist/_agentcoreName-DZDWEYD3.js +0 -0
- package/dist/_twak-4XF4H5PL.js +0 -0
- package/dist/bag.js +317 -124
- package/dist/chunk-RO726HJG.js +0 -0
- package/dist/{chunk-YFEM4564.js → chunk-TTPOH453.js} +79 -37
- package/dist/chunk-U7IDQ3K5.js +0 -0
- package/dist/{deployCli-NJFCWBSF.js → deployCli-K55GXDVO.js} +1 -1
- package/package.json +11 -12
- package/recipes/runtimes/agentcore/code/{{PKG}}/Dockerfile.tmpl +20 -21
- package/recipes/runtimes/agentcore/code/{{PKG}}/executor.ts.tmpl +36 -0
- package/recipes/runtimes/agentcore/code/{{PKG}}/unifiedMain.ts.tmpl +620 -0
- package/recipes/runtimes/agentcore/recipe.toml +3 -3
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/Dockerfile.tmpl +25 -23
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/agentCard.ts.tmpl +16 -12
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/executor.ts.tmpl +72 -393
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/mcpMain.ts.tmpl +160 -43
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/sellerCore.ts.tmpl +504 -0
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/unifiedMain.ts.tmpl +620 -0
- package/recipes/runtimes/azure-foundry/recipe.toml +19 -11
- package/recipes/x402-buyer/code/{{PKG}}/x402Buyer.ts.tmpl +6 -4
- package/skills/bnbagent-studio.md +2 -2
- package/skills/references/bnbagent-studio-adding-to-project.md +1 -1
- package/skills/references/bnbagent-studio-buying-from-bazaar.md +1 -1
- package/skills/references/bnbagent-studio-operating.md +4 -4
- package/skills/references/bnbagent-studio-scaffolding-agent.md +4 -4
- package/skills/references/bnbagent-studio-selling-via-8183.md +3 -3
- package/skills/references/bnbagent-studio-selling-via-b402.md +2 -2
- package/skills/references/bnbagent-studio-use-aws-agentcore.md +1 -1
- package/skills/references/bnbagent-studio-use-azure-foundry.md +3 -3
- package/skills/references/bnbagent-studio-use-bnb-trial.md +1 -1
- package/skills/references/bnbagent-studio-wiring-llm-tools.md +3 -3
- package/recipes/runtimes/agentcore/code/{{PKG}}/main.ts.tmpl +0 -347
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/foundryMain.ts.tmpl +0 -422
- package/recipes/runtimes/azure-foundry/code/{{PKG}}/main.ts.tmpl +0 -196
|
@@ -1,422 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Azure AI Foundry Hosted Agents deploy host — the ERC-8183 seller over A2A.
|
|
3
|
-
*
|
|
4
|
-
* Generated by `bag init --runtime azure-foundry`. This is the
|
|
5
|
-
* Foundry-hosted counterpart of the cloud-neutral A2A entrypoint
|
|
6
|
-
* (`main.ts`). The deploy spec declares Foundry's minimal pass-through
|
|
7
|
-
* `invocations` protocol for user-owned Azure and the `responses` protocol
|
|
8
|
-
* for managed incoming A2A. This host implements both documented container
|
|
9
|
-
* endpoints on :8088 plus GET /readiness.
|
|
10
|
-
*
|
|
11
|
-
* ## How A2A reaches the seller skills on Foundry
|
|
12
|
-
*
|
|
13
|
-
* The seller's two skills live behind the provider's pass-through invoke
|
|
14
|
-
* path. {@link SkillRouter} parses the inbound user text as
|
|
15
|
-
* a JSON skill envelope (`{"skill": "negotiate", ...}` /
|
|
16
|
-
* `{"skill": "notify_funded", ...}`) and dispatches it through the SHARED
|
|
17
|
-
* `SellerAgentExecutor.dispatch` — the same fixed-code skills (`signing.ts`)
|
|
18
|
-
* the cloud-neutral @a2a-js path (`main.ts`) runs. The result dict is
|
|
19
|
-
* serialised back to text (Foundry's incoming A2A is text-modality only).
|
|
20
|
-
*
|
|
21
|
-
* Money is NEVER decided by the LLM: `negotiate` is rule-based + EIP-191
|
|
22
|
-
* signed in `signing.ts`; the LLM runs ONLY inside the `notify_funded`
|
|
23
|
-
* background work hook (produces the deliverable text). The seller has no
|
|
24
|
-
* free-form chat skill, so a non-envelope message gets a deterministic
|
|
25
|
-
* "unknown skill" reply — no LLM on the request path.
|
|
26
|
-
*
|
|
27
|
-
* LLM wiring: the work hook talks to the project's OWN provider over an
|
|
28
|
-
* OpenAI-compatible client built from env vars `bag deploy --provider azure`
|
|
29
|
-
* injects (`BNBAGENT_LLM_BASE_URL` / `BNBAGENT_LLM_MODEL` plus
|
|
30
|
-
* `BNBAGENT_LLM_API_KEY`). Wallet material and provider/storage secrets are
|
|
31
|
-
* stored by bnbagent-deploy in a Foundry CustomKeys project connection and
|
|
32
|
-
* arrive as environment variables before the process starts.
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
import { randomUUID } from "node:crypto";
|
|
36
|
-
import { pathToFileURL } from "node:url";
|
|
37
|
-
import { createOpenAI } from "@ai-sdk/openai";
|
|
38
|
-
import { loadStudioToml } from "@bnbagent/studio-runtime/config";
|
|
39
|
-
import {
|
|
40
|
-
ensureAltanaSessionLoaded,
|
|
41
|
-
ensureKeystoreMaterialized,
|
|
42
|
-
ensureTwakMaterialized,
|
|
43
|
-
} from "@bnbagent/studio-runtime/wallet";
|
|
44
|
-
import { generateText } from "ai";
|
|
45
|
-
import express from "express";
|
|
46
|
-
import { type RunWork, SellerAgentExecutor } from "./executor.js";
|
|
47
|
-
|
|
48
|
-
const APP_NAME = "agent";
|
|
49
|
-
|
|
50
|
-
// LLM wiring `bag deploy --provider azure` injects so the work hook talks to the
|
|
51
|
-
// project's OWN provider (studio.toml [llm] — pieverse-llm / openrouter /
|
|
52
|
-
// openai), all OpenAI-compatible. base URL + model ride the runtime env the
|
|
53
|
-
// deploy injects; the API key comes from the provider-managed CustomKeys
|
|
54
|
-
// connection and is present in the environment before this process starts.
|
|
55
|
-
const LLM_BASE_URL_ENV = "BNBAGENT_LLM_BASE_URL";
|
|
56
|
-
const LLM_MODEL_ENV = "BNBAGENT_LLM_MODEL";
|
|
57
|
-
const LLM_API_KEY_ENV = "BNBAGENT_LLM_API_KEY";
|
|
58
|
-
|
|
59
|
-
const log = {
|
|
60
|
-
info: (msg: string) => console.log(`[seller-agent.foundry] ${msg}`),
|
|
61
|
-
warn: (msg: string) => console.warn(`[seller-agent.foundry] WARNING ${msg}`),
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Drop a blank APPLICATIONINSIGHTS_CONNECTION_STRING (design §9.3).
|
|
66
|
-
*
|
|
67
|
-
* The Azure Monitor exporter parses this env var eagerly; an EMPTY string
|
|
68
|
-
* (vs an absent var) crashes its parser. The Hosted Agents runtime can
|
|
69
|
-
* inject it empty when monitoring is not configured, so remove it when
|
|
70
|
-
* blank before any telemetry init.
|
|
71
|
-
*/
|
|
72
|
-
function scrubBlankAppInsights(): void {
|
|
73
|
-
const val = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING;
|
|
74
|
-
if (val !== undefined && val.trim() === "") {
|
|
75
|
-
delete process.env.APPLICATIONINSIGHTS_CONNECTION_STRING;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Build the work hook's OpenAI-compatible model from the injected LLM
|
|
81
|
-
* config.
|
|
82
|
-
*
|
|
83
|
-
* Reads three fixed env vars `bag deploy --provider azure` populates from studio.toml
|
|
84
|
-
* `[llm]`: base URL + model (runtime env, plaintext) and the API key (the
|
|
85
|
-
* CustomKeys connection). Used ONLY by the notify_funded background work
|
|
86
|
-
* hook to produce the deliverable text.
|
|
87
|
-
*/
|
|
88
|
-
function buildChatModel() {
|
|
89
|
-
const baseUrl = process.env[LLM_BASE_URL_ENV];
|
|
90
|
-
const model = process.env[LLM_MODEL_ENV];
|
|
91
|
-
const apiKey = process.env[LLM_API_KEY_ENV];
|
|
92
|
-
const missing = (
|
|
93
|
-
[
|
|
94
|
-
[LLM_BASE_URL_ENV, baseUrl],
|
|
95
|
-
[LLM_MODEL_ENV, model],
|
|
96
|
-
[LLM_API_KEY_ENV, apiKey],
|
|
97
|
-
] as const
|
|
98
|
-
)
|
|
99
|
-
.filter(([, value]) => !value)
|
|
100
|
-
.map(([name]) => name);
|
|
101
|
-
if (missing.length > 0) {
|
|
102
|
-
throw new Error(
|
|
103
|
-
`LLM is not configured: missing ${missing.join(", ")}. ` +
|
|
104
|
-
"`bag deploy --provider azure` seeds base URL + model from studio.toml [llm] and " +
|
|
105
|
-
"the API key from the Foundry CustomKeys connection — re-deploy " +
|
|
106
|
-
"(or export these vars for a local run).",
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
return createOpenAI({ baseURL: baseUrl, apiKey }).chat(model as string);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Deliverable `generator` label: this seller's own name from studio.toml. */
|
|
113
|
-
function generatorTag(): string {
|
|
114
|
-
let name = "";
|
|
115
|
-
try {
|
|
116
|
-
const cfg = loadStudioToml();
|
|
117
|
-
name = String(((cfg.project ?? {}) as Record<string, unknown>).name ?? "");
|
|
118
|
-
} catch {
|
|
119
|
-
// a metadata label must never break delivery
|
|
120
|
-
return APP_NAME;
|
|
121
|
-
}
|
|
122
|
-
return name.endsWith("-agent")
|
|
123
|
-
? name.slice(0, -"-agent".length)
|
|
124
|
-
: name || APP_NAME;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** studio.toml `[network].default` (best-effort; used by the funded sweep). */
|
|
128
|
-
function defaultNetwork(): string {
|
|
129
|
-
try {
|
|
130
|
-
const cfg = loadStudioToml();
|
|
131
|
-
return String(
|
|
132
|
-
((cfg.network ?? {}) as Record<string, unknown>).default ?? "bsc-testnet",
|
|
133
|
-
);
|
|
134
|
-
} catch {
|
|
135
|
-
return "bsc-testnet";
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Build the SHARED seller executor with an AI-SDK-backed work hook.
|
|
141
|
-
*
|
|
142
|
-
* `negotiate` is rule-based (no LLM); the work hook is called ONLY by the
|
|
143
|
-
* background `notify_funded` delivery to produce the deliverable text. We
|
|
144
|
-
* back it with the project's own OpenAI-compatible provider (the same
|
|
145
|
-
* wiring the Foundry deploy already injects), so no extra LLM secrets
|
|
146
|
-
* beyond the `BNBAGENT_LLM_*` the deploy injects are needed.
|
|
147
|
-
*/
|
|
148
|
-
export function buildExecutor(): SellerAgentExecutor {
|
|
149
|
-
// `negotiate` is fixed code and does not need an LLM. Resolve the model
|
|
150
|
-
// lazily only when funded work actually runs so the Foundry process can
|
|
151
|
-
// become ready (and negotiation can work) even when an operator deliberately
|
|
152
|
-
// bypasses the deploy-time LLM readiness gate for diagnosis.
|
|
153
|
-
let model: ReturnType<typeof buildChatModel> | undefined;
|
|
154
|
-
const runWork: RunWork = async (prompt, { abortSignal }) => {
|
|
155
|
-
model ??= buildChatModel();
|
|
156
|
-
// One-shot: each funded job is independent, so no session is threaded.
|
|
157
|
-
const result = await generateText({
|
|
158
|
-
model,
|
|
159
|
-
system:
|
|
160
|
-
"You are a seller agent. You do the actual work once a job is funded. " +
|
|
161
|
-
"Be concrete, complete, and self-contained. If a paid-data tool such " +
|
|
162
|
-
"as `buy_with_x402` is available to you, USE IT to fetch the data a " +
|
|
163
|
-
"task needs — those merchants (e.g. CoinMarketCap) charge via on-chain " +
|
|
164
|
-
"wallet payment, NOT an API key; never reply that you cannot complete " +
|
|
165
|
-
"the task for lack of an API key.",
|
|
166
|
-
prompt,
|
|
167
|
-
abortSignal,
|
|
168
|
-
});
|
|
169
|
-
return result.text.trim();
|
|
170
|
-
};
|
|
171
|
-
return new SellerAgentExecutor({
|
|
172
|
-
runWork,
|
|
173
|
-
generator: generatorTag(),
|
|
174
|
-
network: defaultNetwork(),
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* The skill-router shim routing Foundry invocations to seller skills.
|
|
180
|
-
*
|
|
181
|
-
* The pass-through host receives a JSON body whose `input` is the serialized
|
|
182
|
-
* skill envelope. We parse that text and
|
|
183
|
-
* dispatch it DETERMINISTICALLY through `SellerAgentExecutor.dispatch`
|
|
184
|
-
* (signing.ts fixed code; LLM only inside the notify_funded background
|
|
185
|
-
* work). The result is serialised back to text. A non-envelope message
|
|
186
|
-
* returns the same "unknown skill" reply the @a2a-js path gives — there is
|
|
187
|
-
* no free-form chat skill.
|
|
188
|
-
*/
|
|
189
|
-
export class SkillRouter {
|
|
190
|
-
readonly name: string;
|
|
191
|
-
readonly description =
|
|
192
|
-
"ERC-8183 seller agent (negotiate + notify_funded) over A2A.";
|
|
193
|
-
private readonly executor: SellerAgentExecutor;
|
|
194
|
-
|
|
195
|
-
constructor(executor: SellerAgentExecutor, opts: { name?: string } = {}) {
|
|
196
|
-
this.executor = executor;
|
|
197
|
-
this.name = opts.name ?? "seller_agent";
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/** Route one inbound text turn to the seller skills; returns JSON text. */
|
|
201
|
-
async run(text: string | null | undefined): Promise<string> {
|
|
202
|
-
const envelope = extractEnvelope(text);
|
|
203
|
-
const result =
|
|
204
|
-
envelope === null
|
|
205
|
-
? {
|
|
206
|
-
error:
|
|
207
|
-
'expected a JSON skill envelope, e.g. {"skill": "negotiate", ...}',
|
|
208
|
-
skills: ["negotiate", "notify_funded"],
|
|
209
|
-
}
|
|
210
|
-
: await this.executor.dispatch(envelope);
|
|
211
|
-
return JSON.stringify(result);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Parse inbound user text into a `{"skill": ...}` dict, or null.
|
|
217
|
-
*
|
|
218
|
-
* Foundry's incoming A2A is text-modality only, so the buyer sends the skill
|
|
219
|
-
* envelope as a JSON string. Non-JSON / non-object text → null (the caller
|
|
220
|
-
* replies with a deterministic "unknown skill").
|
|
221
|
-
*/
|
|
222
|
-
export function extractEnvelope(
|
|
223
|
-
text: string | null | undefined,
|
|
224
|
-
): Record<string, unknown> | null {
|
|
225
|
-
if (!text) {
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
let obj: unknown;
|
|
229
|
-
try {
|
|
230
|
-
obj = JSON.parse(text);
|
|
231
|
-
} catch {
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
234
|
-
return obj !== null && typeof obj === "object" && !Array.isArray(obj)
|
|
235
|
-
? (obj as Record<string, unknown>)
|
|
236
|
-
: null;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/** Extract the latest text turn from an OpenAI Responses request. Foundry's
|
|
240
|
-
* incoming A2A adapter is text-only and projects the caller message here. */
|
|
241
|
-
export function responsesInputText(input: unknown): string | null {
|
|
242
|
-
if (typeof input === "string") {
|
|
243
|
-
return input;
|
|
244
|
-
}
|
|
245
|
-
if (!Array.isArray(input)) {
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
248
|
-
const texts: string[] = [];
|
|
249
|
-
for (const item of input) {
|
|
250
|
-
if (typeof item === "string") {
|
|
251
|
-
texts.push(item);
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
if (!item || typeof item !== "object") {
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
const content = (item as Record<string, unknown>).content;
|
|
258
|
-
if (typeof content === "string") {
|
|
259
|
-
texts.push(content);
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
if (!Array.isArray(content)) {
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
for (const part of content) {
|
|
266
|
-
if (part && typeof part === "object") {
|
|
267
|
-
const text = (part as Record<string, unknown>).text;
|
|
268
|
-
if (typeof text === "string") texts.push(text);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
return texts.at(-1) ?? null;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function responseId(prefix: string): string {
|
|
276
|
-
return `${prefix}_${randomUUID().replaceAll("-", "")}`;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function responseEnvelope(text: string, model: string) {
|
|
280
|
-
const responseIdValue = responseId("resp");
|
|
281
|
-
const messageId = responseId("msg");
|
|
282
|
-
const part = { type: "output_text", annotations: [], logprobs: [], text };
|
|
283
|
-
const item = {
|
|
284
|
-
id: messageId,
|
|
285
|
-
type: "message",
|
|
286
|
-
status: "completed",
|
|
287
|
-
role: "assistant",
|
|
288
|
-
content: [part],
|
|
289
|
-
};
|
|
290
|
-
return {
|
|
291
|
-
id: responseIdValue,
|
|
292
|
-
object: "response",
|
|
293
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
294
|
-
status: "completed",
|
|
295
|
-
error: null,
|
|
296
|
-
incomplete_details: null,
|
|
297
|
-
instructions: null,
|
|
298
|
-
max_output_tokens: null,
|
|
299
|
-
model,
|
|
300
|
-
output: [item],
|
|
301
|
-
output_text: text,
|
|
302
|
-
parallel_tool_calls: true,
|
|
303
|
-
previous_response_id: null,
|
|
304
|
-
reasoning: { effort: null, summary: null },
|
|
305
|
-
store: true,
|
|
306
|
-
temperature: 1,
|
|
307
|
-
text: { format: { type: "text" } },
|
|
308
|
-
tool_choice: "auto",
|
|
309
|
-
tools: [],
|
|
310
|
-
top_p: 1,
|
|
311
|
-
truncation: "disabled",
|
|
312
|
-
usage: {
|
|
313
|
-
input_tokens: 0,
|
|
314
|
-
input_tokens_details: { cached_tokens: 0 },
|
|
315
|
-
output_tokens: 0,
|
|
316
|
-
output_tokens_details: { reasoning_tokens: 0 },
|
|
317
|
-
total_tokens: 0,
|
|
318
|
-
},
|
|
319
|
-
metadata: {},
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function sendStreamingResponse(res: express.Response, response: ReturnType<typeof responseEnvelope>): void {
|
|
324
|
-
const item = response.output[0];
|
|
325
|
-
const part = item.content[0];
|
|
326
|
-
const events = [
|
|
327
|
-
{ type: "response.created", sequence_number: 0, response: { ...response, status: "in_progress", output: [] } },
|
|
328
|
-
{ type: "response.output_item.added", sequence_number: 1, output_index: 0, item: { ...item, status: "in_progress", content: [] } },
|
|
329
|
-
{ type: "response.content_part.added", sequence_number: 2, output_index: 0, item_id: item.id, content_index: 0, part: { ...part, text: "" } },
|
|
330
|
-
{ type: "response.output_text.delta", sequence_number: 3, output_index: 0, item_id: item.id, content_index: 0, delta: part.text },
|
|
331
|
-
{ type: "response.output_text.done", sequence_number: 4, output_index: 0, item_id: item.id, content_index: 0, text: part.text },
|
|
332
|
-
{ type: "response.content_part.done", sequence_number: 5, output_index: 0, item_id: item.id, content_index: 0, part },
|
|
333
|
-
{ type: "response.output_item.done", sequence_number: 6, output_index: 0, item },
|
|
334
|
-
{ type: "response.completed", sequence_number: 7, response },
|
|
335
|
-
];
|
|
336
|
-
res.status(200);
|
|
337
|
-
res.setHeader("content-type", "text/event-stream");
|
|
338
|
-
res.setHeader("cache-control", "no-cache");
|
|
339
|
-
for (const event of events) {
|
|
340
|
-
res.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
341
|
-
}
|
|
342
|
-
res.end("data: [DONE]\n\n");
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
async function main(): Promise<void> {
|
|
346
|
-
scrubBlankAppInsights();
|
|
347
|
-
|
|
348
|
-
// Wallet material arrives through the provider-managed CustomKeys env,
|
|
349
|
-
// never the image; materialize it on disk once before any signing (no-op
|
|
350
|
-
// for the other wallet kind and locally, where it already lives on disk).
|
|
351
|
-
ensureKeystoreMaterialized();
|
|
352
|
-
ensureTwakMaterialized();
|
|
353
|
-
await ensureAltanaSessionLoaded();
|
|
354
|
-
|
|
355
|
-
// Cold-start diagnostic (presence booleans ONLY — never secret values):
|
|
356
|
-
// makes a missing CustomKeys injection obvious in the invoke logs instead
|
|
357
|
-
// of failing opaquely deep inside the twak CLI ("No wallet password
|
|
358
|
-
// found").
|
|
359
|
-
log.info(
|
|
360
|
-
"runtime-secrets cold-start: " +
|
|
361
|
-
`TWAK_WALLET_PASSWORD=${Boolean(process.env.TWAK_WALLET_PASSWORD)} ` +
|
|
362
|
-
`TWAK_ACCESS_ID=${Boolean(process.env.TWAK_ACCESS_ID)} ` +
|
|
363
|
-
`TWAK_CREDENTIALS_JSON=${Boolean(process.env.TWAK_CREDENTIALS_JSON)} ` +
|
|
364
|
-
`LLM_API_KEY=${Boolean(process.env[LLM_API_KEY_ENV])}`,
|
|
365
|
-
);
|
|
366
|
-
|
|
367
|
-
const router = new SkillRouter(buildExecutor(), { name: generatorTag() });
|
|
368
|
-
|
|
369
|
-
// Foundry Invocations is a pass-through JSON contract. bnbagent-deploy's
|
|
370
|
-
// positional invoke helper normalizes text to {"input":"..."}; advanced
|
|
371
|
-
// callers may send the same shape directly.
|
|
372
|
-
const app = express();
|
|
373
|
-
app.use(express.json());
|
|
374
|
-
app.post("/invocations", async (req, res) => {
|
|
375
|
-
const input = (req.body ?? {}) as Record<string, unknown>;
|
|
376
|
-
const text = typeof input.input === "string" ? input.input : null;
|
|
377
|
-
res.json({ output: await router.run(text) });
|
|
378
|
-
});
|
|
379
|
-
// The managed platform selects Foundry's Responses protocol so the agent
|
|
380
|
-
// can be exposed through incoming A2A. Keep this adapter small and
|
|
381
|
-
// deterministic: Foundry converts A2A text to Responses input, and the
|
|
382
|
-
// seller result becomes one assistant output_text item.
|
|
383
|
-
app.post("/responses", async (req, res) => {
|
|
384
|
-
const input = (req.body ?? {}) as Record<string, unknown>;
|
|
385
|
-
const output = await router.run(responsesInputText(input.input));
|
|
386
|
-
const response = responseEnvelope(output, typeof input.model === "string" ? input.model : APP_NAME);
|
|
387
|
-
if (input.stream === true) {
|
|
388
|
-
sendStreamingResponse(res, response);
|
|
389
|
-
} else {
|
|
390
|
-
res.json(response);
|
|
391
|
-
}
|
|
392
|
-
});
|
|
393
|
-
// Required Foundry Hosted Agent health contract. The platform will not
|
|
394
|
-
// create a session or forward invocations until this returns HTTP 200.
|
|
395
|
-
app.get("/readiness", (_req, res) => {
|
|
396
|
-
res.json({ status: "READY" });
|
|
397
|
-
});
|
|
398
|
-
app.get("/ping", (_req, res) => {
|
|
399
|
-
res.json({ status: "HEALTHY" });
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
// Foundry's documented default is 8088; PORT remains the platform override.
|
|
403
|
-
const port = Number(process.env.PORT || process.env.AGENT_PORT || "8088");
|
|
404
|
-
const server = app.listen(port, "0.0.0.0", () => {
|
|
405
|
-
log.info(`Invocations + Responses host serving on 0.0.0.0:${port}`);
|
|
406
|
-
});
|
|
407
|
-
process.once("SIGTERM", () => {
|
|
408
|
-
server.close(() => process.exit(0));
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// Run only as an entrypoint, never on import (tests import SkillRouter /
|
|
413
|
-
// extractEnvelope / buildExecutor without starting a server).
|
|
414
|
-
const isMain =
|
|
415
|
-
process.argv[1] !== undefined &&
|
|
416
|
-
import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
417
|
-
if (isMain) {
|
|
418
|
-
main().catch((e) => {
|
|
419
|
-
console.error("[seller-agent.foundry] fatal:", e);
|
|
420
|
-
process.exit(1);
|
|
421
|
-
});
|
|
422
|
-
}
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Single A2A seller agent entrypoint — cloud-neutral (azure-foundry runtime).
|
|
3
|
-
*
|
|
4
|
-
* The x402 seller is intentionally dormant on Azure Foundry in v1. Use the
|
|
5
|
-
* platform deployment target for the anonymous HTTP-envelope tunnel.
|
|
6
|
-
*
|
|
7
|
-
* This is the `--protocol A2A` peer of `mcpMain.ts` for the azure-foundry
|
|
8
|
-
* runtime. It serves the SAME two ERC-8183 seller skills (`negotiate` +
|
|
9
|
-
* `notify_funded`) over the A2A protocol as the AgentCore entrypoint, using
|
|
10
|
-
* the official `@a2a-js/sdk` express integration directly, so this agent
|
|
11
|
-
* carries no cloud-vendor serving dependency (commitments #4/#5 — jump-ship,
|
|
12
|
-
* no lock-in). The executor + agent card are shared, unmodified, with the
|
|
13
|
-
* Foundry deploy host (`executor.ts` / `agentCard.ts`).
|
|
14
|
-
*
|
|
15
|
-
* Scope: the azure-foundry DEPLOY
|
|
16
|
-
* host speaks the Invocations protocol (`foundryMain.ts`, what Foundry's
|
|
17
|
-
* invoke path calls). THIS A2A entrypoint is for LOCAL run / dev (`bag dev`) and
|
|
18
|
-
* the eventual cross-cloud A2A transport — `bag dev` runs `node main.js`
|
|
19
|
-
* directly, the same way it runs the AgentCore CodeZip. There is no
|
|
20
|
-
* AgentCore scale-to-zero here, so no HEALTHY_BUSY `/ping` is needed: the
|
|
21
|
-
* node process stays up, so the background `notify_funded` delivery
|
|
22
|
-
* completes on its own. A STATIC 200 `/ping` is still served purely so
|
|
23
|
-
* health-check clients that probe `/ping` behave uniformly with the
|
|
24
|
-
* AgentCore runtime — it is a compatibility shim, never a busy signal
|
|
25
|
-
* (liveness in A2A is the card).
|
|
26
|
-
*
|
|
27
|
-
* A2A skills (executor.ts):
|
|
28
|
-
*
|
|
29
|
-
* negotiate → read the FIXED list price → CLAMP to [min,max] → EIP-191 SIGN
|
|
30
|
-
* the offer (no LLM, no tools) → return the signed offer (or reject)
|
|
31
|
-
* notify_funded → re-verify the funded job on-chain (fast) → ACK accepted at once,
|
|
32
|
-
* then in the BACKGROUND: LLM work → manifest → storage →
|
|
33
|
-
* submitResult (SIGN + broadcast). The buyer polls the chain for
|
|
34
|
-
* the deliverable.
|
|
35
|
-
*
|
|
36
|
-
* ## Boundaries (do NOT cross — they are the whole point)
|
|
37
|
-
*
|
|
38
|
-
* - The agent does ALL deterministic SIGNING. ALL signing is FIXED code in
|
|
39
|
-
* `signing.ts` — NEVER an LLM-callable tool (money never in the LLM).
|
|
40
|
-
* - The price is a FIXED list price from studio.toml (clamped before
|
|
41
|
-
* signing) — the LLM never prices; it only PRODUCES the work text in
|
|
42
|
-
* `notify_funded`.
|
|
43
|
-
* - Chain access for the LLM is READ-ONLY tools only (`tools.ts`).
|
|
44
|
-
* - `settle` (claim payment after the dispute window) is operator-driven —
|
|
45
|
-
* run `bag erc8183 settle <job_id>`; it is deliberately NOT an A2A skill.
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
import { pathToFileURL } from "node:url";
|
|
49
|
-
import { DefaultRequestHandler, InMemoryTaskStore } from "@a2a-js/sdk/server";
|
|
50
|
-
import {
|
|
51
|
-
agentCardHandler,
|
|
52
|
-
jsonRpcHandler,
|
|
53
|
-
UserBuilder,
|
|
54
|
-
} from "@a2a-js/sdk/server/express";
|
|
55
|
-
import { loadStudioToml } from "@bnbagent/studio-runtime/config";
|
|
56
|
-
import {
|
|
57
|
-
ensureAltanaSessionLoaded,
|
|
58
|
-
ensureKeystoreMaterialized,
|
|
59
|
-
ensureTwakMaterialized,
|
|
60
|
-
} from "@bnbagent/studio-runtime/wallet";
|
|
61
|
-
import { generateText, stepCountIs } from "ai";
|
|
62
|
-
import express from "express";
|
|
63
|
-
import { buildAgentCard } from "./agentCard.js";
|
|
64
|
-
import { type RunWork, SellerAgentExecutor } from "./executor.js";
|
|
65
|
-
import { buildModel } from "./model.js";
|
|
66
|
-
import { LLM_READ_TOOLS } from "./tools.js";
|
|
67
|
-
|
|
68
|
-
const APP_NAME = "agent";
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Deliverable `generator` label: this seller's own name, read from
|
|
72
|
-
* studio.toml `[project].name` (minus the `-agent` suffix). Best-effort —
|
|
73
|
-
* falls back to `APP_NAME` if the config can't be read.
|
|
74
|
-
*/
|
|
75
|
-
function generatorTag(): string {
|
|
76
|
-
let name = "";
|
|
77
|
-
try {
|
|
78
|
-
const cfg = loadStudioToml();
|
|
79
|
-
name = String(((cfg.project ?? {}) as Record<string, unknown>).name ?? "");
|
|
80
|
-
} catch {
|
|
81
|
-
// a metadata label must never break delivery
|
|
82
|
-
return APP_NAME;
|
|
83
|
-
}
|
|
84
|
-
return name.endsWith("-agent")
|
|
85
|
-
? name.slice(0, -"-agent".length)
|
|
86
|
-
: name || APP_NAME;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** studio.toml `[network].default` (best-effort; used by the funded sweep). */
|
|
90
|
-
function defaultNetwork(): string {
|
|
91
|
-
try {
|
|
92
|
-
const cfg = loadStudioToml();
|
|
93
|
-
return String(
|
|
94
|
-
((cfg.network ?? {}) as Record<string, unknown>).default ?? "bsc-testnet",
|
|
95
|
-
);
|
|
96
|
-
} catch {
|
|
97
|
-
return "bsc-testnet";
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ── One-shot LLM helper (the executor's work hook) ────────────────────────────
|
|
102
|
-
// The LLM runs ONLY in the background `notify_funded` work (the value hook).
|
|
103
|
-
// `negotiate` is rule-based and never touches the LLM. The read-only chain
|
|
104
|
-
// tools are attached so the work can read on-chain context. Signing / x402 /
|
|
105
|
-
// settle are NEVER tools — they are fixed code in signing.ts.
|
|
106
|
-
export function buildRunWork(): RunWork {
|
|
107
|
-
// The model is resolved LAZILY on first delivery, not at boot: a seller
|
|
108
|
-
// with no provider key yet must still serve negotiate (which never calls
|
|
109
|
-
// the LLM) — missing-key errors surface at notify_funded delivery time.
|
|
110
|
-
let model: ReturnType<typeof buildModel> | undefined;
|
|
111
|
-
return async (prompt, { abortSignal }) => {
|
|
112
|
-
model ??= buildModel(); // managed model with the auto-renew hook (work only)
|
|
113
|
-
const result = await generateText({
|
|
114
|
-
model,
|
|
115
|
-
system:
|
|
116
|
-
"You are a seller agent. You do the actual work once a job is funded. " +
|
|
117
|
-
"Be concrete and concise. Use the read-only chain tools when on-chain " +
|
|
118
|
-
"context helps. If a paid-data tool such as `buy_with_x402` is available " +
|
|
119
|
-
"to you, USE IT to fetch the data a task needs — those merchants (e.g. " +
|
|
120
|
-
"CoinMarketCap) charge via on-chain wallet payment, NOT an API key; never " +
|
|
121
|
-
"reply that you cannot complete the task for lack of an API key.",
|
|
122
|
-
prompt,
|
|
123
|
-
tools: LLM_READ_TOOLS, // READ-ONLY; signing is never an LLM tool
|
|
124
|
-
stopWhen: stepCountIs(8),
|
|
125
|
-
abortSignal,
|
|
126
|
-
});
|
|
127
|
-
return result.text.trim();
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ── serving ───────────────────────────────────────────────────────────────────
|
|
132
|
-
|
|
133
|
-
async function main(): Promise<void> {
|
|
134
|
-
// Wallet material is NEVER bundled into the deploy artifact. `bag deploy`
|
|
135
|
-
// injects it through a Foundry CustomKeys connection before process start;
|
|
136
|
-
// these calls materialize it on disk before signing. Each is a no-op for
|
|
137
|
-
// the other wallet kind and locally, where it already lives on disk.
|
|
138
|
-
ensureKeystoreMaterialized();
|
|
139
|
-
ensureTwakMaterialized();
|
|
140
|
-
await ensureAltanaSessionLoaded();
|
|
141
|
-
|
|
142
|
-
// The executor backs the seller skills with signing.ts fixed code (NEVER
|
|
143
|
-
// an LLM tool). The @a2a-js/sdk express app exposes the agent card at
|
|
144
|
-
// /.well-known/agent-card.json and JSON-RPC message/send at /.
|
|
145
|
-
const executor = new SellerAgentExecutor({
|
|
146
|
-
runWork: buildRunWork(),
|
|
147
|
-
generator: generatorTag(),
|
|
148
|
-
network: defaultNetwork(),
|
|
149
|
-
});
|
|
150
|
-
const handler = new DefaultRequestHandler(
|
|
151
|
-
buildAgentCard(),
|
|
152
|
-
new InMemoryTaskStore(),
|
|
153
|
-
executor,
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
const app = express();
|
|
157
|
-
|
|
158
|
-
// Static liveness probe. NOT an AgentCore scale-to-zero signal (there is
|
|
159
|
-
// no AgentCore here) and NOT part of the A2A protocol (the agent card is
|
|
160
|
-
// the A2A-native liveness). Served as a plain 200 so health-check clients
|
|
161
|
-
// that probe /ping behave uniformly with the AgentCore runtime.
|
|
162
|
-
app.get("/ping", (_req, res) => {
|
|
163
|
-
res.json({ status: "HEALTHY" });
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
app.use(
|
|
167
|
-
"/.well-known/agent-card.json",
|
|
168
|
-
agentCardHandler({ agentCardProvider: handler }),
|
|
169
|
-
);
|
|
170
|
-
app.use(
|
|
171
|
-
jsonRpcHandler({
|
|
172
|
-
requestHandler: handler,
|
|
173
|
-
userBuilder: UserBuilder.noAuthentication,
|
|
174
|
-
}),
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
// AGENT_PORT is the local-dev port `bag dev` sets (9000 default); PORT
|
|
178
|
-
// wins if a host platform injects it.
|
|
179
|
-
const host = process.env.AGENT_HOST || "0.0.0.0";
|
|
180
|
-
const port = Number(process.env.PORT || process.env.AGENT_PORT || "9000");
|
|
181
|
-
app.listen(port, host, () => {
|
|
182
|
-
console.log(`[seller-agent] A2A serving on ${host}:${port}`);
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Run only as an entrypoint (`node main.js` / `bag dev`), never on import —
|
|
187
|
-
// tests import the builders above without starting a server.
|
|
188
|
-
const isMain =
|
|
189
|
-
process.argv[1] !== undefined &&
|
|
190
|
-
import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
191
|
-
if (isMain) {
|
|
192
|
-
main().catch((e) => {
|
|
193
|
-
console.error("[seller-agent] fatal:", e);
|
|
194
|
-
process.exit(1);
|
|
195
|
-
});
|
|
196
|
-
}
|