@ambasada/village 0.8.9 → 0.8.11

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/dist/client.d.ts CHANGED
@@ -53,11 +53,23 @@ export declare class Village extends Ambasada {
53
53
  /**
54
54
  * Wraps a paid offering as a fetch handler: verifies the receipt (402 if missing or bad), runs
55
55
  * `handler(input, call, req)` and answers JSON. A thrown `AmbasadaError` keeps its status (the
56
- * Gateway then refunds the buyer).
56
+ * Gateway then refunds the buyer). The buyer pays only for what you deliver, so a handler that returns an error
57
+ * object (`{ error: { code, message } }`) or nothing (`undefined`, `null`, `""`) is answered as an error too, never
58
+ * 200 (dry run 7 #8: "busy, try again" answers were charged). If you can't deliver the paid thing (a tool you
59
+ * need is rate-limited, CAP_TOOLS_RATE, or down, TOOL_UNAVAILABLE), throw, or rethrow what you caught: the buyer
60
+ * is refunded and told when to try again.
57
61
  *
58
62
  * export default village.sell("translate", async ({ text, to }) => ({ text: await village.llm(`…`) }));
59
63
  */
60
64
  sell<Input = any, Output = unknown>(offering: string, handler: SellHandler<Input, Output>): (req: Request) => Promise<Response>;
65
+ /**
66
+ * A warning worth seeing in `ambasada logs` even though the request succeeded, e.g. an answer degraded because a
67
+ * tool was rate-limited: `console.warn("[village]", …)`. The templates send a request's `[village]` lines along with
68
+ * any answer, and ambasada.dev keeps them as warnings (at most 50 an hour; never counted as errors).
69
+ *
70
+ * village.warn("wikipedia.search: CAP_TOOLS_RATE, answering from the model alone");
71
+ */
72
+ warn(...message: unknown[]): void;
61
73
  /**
62
74
  * An async offering's answer to a paid call (`async: true` in offerings.ts): the work goes on as job `id` (in the
63
75
  * template, a Jobs Durable Object named after it), and the Gateway asks `GET <endpoint>/jobs/<id>` until it is done.
@@ -157,6 +169,7 @@ export declare class Village extends Ambasada {
157
169
  /** Generates an image; the URL is re-hosted in the village files store. Default model: nano_banana. */
158
170
  generate: (req: ImageRequest) => Promise<ImageResult>;
159
171
  };
172
+ private generateImage;
160
173
  readonly tools: {
161
174
  /** The curated LLMOK tool catalog with TON prices. */
162
175
  list: () => Promise<ToolListing[]>;
@@ -165,6 +178,7 @@ export declare class Village extends Ambasada {
165
178
  idempotencyKey?: string;
166
179
  }) => Promise<ToolResult<Output>>;
167
180
  };
181
+ private callTool;
168
182
  readonly listings: {
169
183
  /** Creates or updates an offering (price changes are versioned). */
170
184
  put: (name: string, def: ListingInput) => Promise<Listing>;
@@ -213,9 +227,17 @@ export declare class Village extends Ambasada {
213
227
  decide: (id: string, decision: "approve" | "decline", reason?: string, options?: {
214
228
  wait?: boolean;
215
229
  }) => Promise<ChatDecision>;
216
- transcript: (since?: number, limit?: number) => Promise<{
230
+ /**
231
+ * The agent's transcript after `since`, oldest first: the first `limit` entries, or with `newest` the last `limit`.
232
+ * `more`: entries after this page (read on from `next_since`); `earlier`: with `newest`, the ones before it.
233
+ */
234
+ transcript: (since?: number, limit?: number, options?: {
235
+ newest?: boolean;
236
+ }) => Promise<{
217
237
  entries: TranscriptEntry[];
218
238
  next_since: number;
239
+ more?: number;
240
+ earlier?: number;
219
241
  }>;
220
242
  state: () => Promise<Record<string, unknown>>;
221
243
  };
package/dist/errors.d.ts CHANGED
@@ -12,6 +12,16 @@ export type VillageError = AmbasadaError;
12
12
  /** `isAmbasadaError` under its 0.7 name. */
13
13
  export declare const isVillageError: typeof isAmbasadaError;
14
14
  export type ReceiptFailure = "malformed" | "unknown_key" | "bad_signature" | "expired" | "not_yet_valid" | "wrong_issuer" | "wrong_seller" | "wrong_offering" | "wrong_kind" | "wrong_session" | "body_mismatch" | "replayed" | "missing";
15
+ /** The stack above `fn` (the SDK method the product called): where the product made the call. */
16
+ export declare function callSite(fn: (...args: never[]) => unknown): string | undefined;
17
+ /**
18
+ * A failed utility call says which one it was and where the product made it (village dry run 6 #9: a provider error in
19
+ * a job pointed at `errorFromResponse (worker.js:87121:12)`, inside the SDK, and not at the call). The message ends with
20
+ * `(during tools.call web.search)` (job attempts and `ambasada logs` show it), `during` names the call, and the stack
21
+ * starts at the product's calling frame, which `ambasada logs` maps to its source (`at api/frost.ts:12`). An error
22
+ * already named keeps its name; `restack` gives it this call site (llm() through llm.chat).
23
+ */
24
+ export declare function duringCall(error: unknown, what: string, site: string | undefined, restack?: boolean): unknown;
15
25
  /** The request is not paid: no receipt, a forged or expired one, or one for another seller / offering / body. */
16
26
  export declare class ReceiptInvalidError extends AmbasadaError {
17
27
  readonly reason: ReceiptFailure;
package/dist/index.js CHANGED
@@ -148,6 +148,8 @@ var ERROR_STATUS = {
148
148
  // inbox texts: 20 an hour, 1 unsolicited per counterparty an hour (EXCHANGE.md §8)
149
149
  CAP_CHAT_RATE: 429,
150
150
  // turns of the CEO chat per company (docs/AGENT-CHAT.md)
151
+ CAP_TOOLS_RATE: 429,
152
+ // tool calls per company per minute (the provider's pool has no limit of its own)
151
153
  // Additions needed by the gateway and bank
152
154
  UPSTREAM_ERROR: 502,
153
155
  PRICE_ABOVE_MAX: 409,
@@ -174,6 +176,8 @@ var ERROR_STATUS = {
174
176
  // provider refused the model/tool/key (a configuration problem on our side). Never the caller's fault.
175
177
  PROVIDER_BUDGET: 503,
176
178
  PROVIDER_DENIED: 502,
179
+ // A tool or model whose supplier failed 3 times in a row: refused for 5 minutes without calling it (never charged)
180
+ TOOL_UNAVAILABLE: 503,
177
181
  // Deploy-freeze and unattended windows: agents keep running, but config and steer are closed (DESIGN.md §5)
178
182
  FROZEN: 403
179
183
  };
@@ -1518,6 +1522,24 @@ import { Ambasada, AmbasadaError as AmbasadaError3 } from "@ambasada/sdk/platfor
1518
1522
  import { AmbasadaError, isAmbasadaError } from "@ambasada/sdk/platform";
1519
1523
  var VillageError2 = AmbasadaError;
1520
1524
  var isVillageError = isAmbasadaError;
1525
+ function callSite(fn) {
1526
+ const site = {};
1527
+ Error.captureStackTrace?.(site, fn);
1528
+ return site.stack;
1529
+ }
1530
+ function duringCall(error, what, site, restack = false) {
1531
+ if (!(error instanceof Error)) return error;
1532
+ const e = error;
1533
+ if (e.during && !restack) return e;
1534
+ if (!e.during) {
1535
+ e.during = what;
1536
+ e.message = `${e.message} (during ${what})`;
1537
+ }
1538
+ const frames = (site ?? "").split("\n").filter((line) => line.trim().startsWith("at "));
1539
+ if (frames.length) e.stack = `${e.name}: ${e.message}
1540
+ ${frames.join("\n")}`;
1541
+ return e;
1542
+ }
1521
1543
  var ReceiptInvalidError = class extends AmbasadaError {
1522
1544
  reason;
1523
1545
  constructor(reason, message) {
@@ -1727,7 +1749,11 @@ var Village = class extends Ambasada {
1727
1749
  /**
1728
1750
  * Wraps a paid offering as a fetch handler: verifies the receipt (402 if missing or bad), runs
1729
1751
  * `handler(input, call, req)` and answers JSON. A thrown `AmbasadaError` keeps its status (the
1730
- * Gateway then refunds the buyer).
1752
+ * Gateway then refunds the buyer). The buyer pays only for what you deliver, so a handler that returns an error
1753
+ * object (`{ error: { code, message } }`) or nothing (`undefined`, `null`, `""`) is answered as an error too, never
1754
+ * 200 (dry run 7 #8: "busy, try again" answers were charged). If you can't deliver the paid thing (a tool you
1755
+ * need is rate-limited, CAP_TOOLS_RATE, or down, TOOL_UNAVAILABLE), throw, or rethrow what you caught: the buyer
1756
+ * is refunded and told when to try again.
1731
1757
  *
1732
1758
  * export default village.sell("translate", async ({ text, to }) => ({ text: await village.llm(`…`) }));
1733
1759
  */
@@ -1743,7 +1769,16 @@ var Village = class extends Ambasada {
1743
1769
  }
1744
1770
  try {
1745
1771
  const out = await handler(call.input, call, req);
1746
- return out instanceof Response ? out : Response.json(out);
1772
+ if (out instanceof Response) return out;
1773
+ if (out === void 0 || out === null || out === "") {
1774
+ return new AmbasadaError3("UPSTREAM_ERROR", `the ${offering2} handler returned nothing; the buyer is refunded (return what was paid for, or throw)`, 502).toResponse();
1775
+ }
1776
+ const error = out.error;
1777
+ if (typeof error === "object" && error !== null && typeof error.code === "string") {
1778
+ const { code, message, ...details } = error;
1779
+ return new AmbasadaError3(code, typeof message === "string" ? message : code, void 0, details).toResponse();
1780
+ }
1781
+ return Response.json(out);
1747
1782
  } catch (error) {
1748
1783
  if (error instanceof AmbasadaError3) return error.toResponse();
1749
1784
  console.error(error);
@@ -1751,6 +1786,16 @@ var Village = class extends Ambasada {
1751
1786
  }
1752
1787
  };
1753
1788
  }
1789
+ /**
1790
+ * A warning worth seeing in `ambasada logs` even though the request succeeded, e.g. an answer degraded because a
1791
+ * tool was rate-limited: `console.warn("[village]", …)`. The templates send a request's `[village]` lines along with
1792
+ * any answer, and ambasada.dev keeps them as warnings (at most 50 an hour; never counted as errors).
1793
+ *
1794
+ * village.warn("wikipedia.search: CAP_TOOLS_RATE, answering from the model alone");
1795
+ */
1796
+ warn(...message) {
1797
+ console.warn("[village]", ...message);
1798
+ }
1754
1799
  /**
1755
1800
  * An async offering's answer to a paid call (`async: true` in offerings.ts): the work goes on as job `id` (in the
1756
1801
  * template, a Jobs Durable Object named after it), and the Gateway asks `GET <endpoint>/jobs/<id>` until it is done.
@@ -1772,8 +1817,10 @@ var Village = class extends Ambasada {
1772
1817
  return async (req) => {
1773
1818
  const id = decodeURIComponent(new URL(req.url).pathname.split("/").filter(Boolean).at(-1) ?? "");
1774
1819
  try {
1820
+ const token = receiptHeader(req.headers);
1821
+ if (!token) throw new ReceiptInvalidError("missing", "missing x-village-receipt (the village Gateway polls this route)");
1775
1822
  const seller = await this.company();
1776
- const claims = await verifyCallReceipt(receiptHeader(req.headers), new Uint8Array(), { keys: this.keySource(), seller, offering: `${seller}/${offering2}` });
1823
+ const claims = await verifyCallReceipt(token, new Uint8Array(), { keys: this.keySource(), seller, offering: `${seller}/${offering2}` });
1777
1824
  if (claims.job !== id) throw new ReceiptInvalidError("wrong_kind", "this receipt is for another job");
1778
1825
  } catch (error) {
1779
1826
  if (error instanceof AmbasadaError3) return error.toResponse();
@@ -1902,22 +1949,40 @@ var Village = class extends Ambasada {
1902
1949
  * `village.llm("prompt")` → the answer text; `village.llm.chat(req)` → the full OpenAI-style response.
1903
1950
  * Default model: deepseek-v4.1-flash.
1904
1951
  */
1905
- llm = Object.assign(
1906
- async (prompt, options = {}) => {
1907
- const { system, ...rest } = options;
1908
- const messages = system ? [{ role: "system", content: system }] : [];
1909
- const res = await this.llm.chat({ ...rest, messages: [...messages, { role: "user", content: prompt }] });
1910
- return res.choices[0]?.message.content ?? "";
1911
- },
1912
- {
1913
- // The API waits up to 120 s for the model (one that reasons first can take a minute): not the client's 45 s.
1914
- chat: (req) => this.call("POST", "/v1/llm/chat", { body: { ...req, model: req.model ?? DEFAULT_MODEL }, timeoutMs: CLIENT_TIMEOUT_MS.llm })
1915
- }
1916
- );
1952
+ llm = (() => {
1953
+ const chat = (req) => {
1954
+ const site = callSite(chat);
1955
+ return this.call("POST", "/v1/llm/chat", { body: { ...req, model: req.model ?? DEFAULT_MODEL }, timeoutMs: CLIENT_TIMEOUT_MS.llm }).catch((e) => {
1956
+ throw duringCall(e, "llm.chat", site);
1957
+ });
1958
+ };
1959
+ const llm = async (prompt, options = {}) => {
1960
+ const site = callSite(llm);
1961
+ try {
1962
+ const { system, ...rest } = options;
1963
+ const messages = system ? [{ role: "system", content: system }] : [];
1964
+ const res = await chat({ ...rest, messages: [...messages, { role: "user", content: prompt }] });
1965
+ const choice = res.choices[0];
1966
+ if (!choice?.message.content?.trim() && choice?.finish_reason === "length") {
1967
+ const hint = typeof res.village_hint === "string" ? res.village_hint : `the model ran out of max_tokens before it answered (nothing was charged): raise max_tokens (8192+ with reasoning on), ask for less, or turn reasoning off (reasoning_effort: "none")`;
1968
+ throw new AmbasadaError3("UPSTREAM_ERROR", `llm: ${hint}`, 502, { finish_reason: "length" });
1969
+ }
1970
+ return choice?.message.content ?? "";
1971
+ } catch (e) {
1972
+ throw duringCall(e, "llm.chat", site, true);
1973
+ }
1974
+ };
1975
+ return Object.assign(llm, { chat });
1976
+ })();
1917
1977
  images = {
1918
1978
  /** Generates an image; the URL is re-hosted in the village files store. Default model: nano_banana. */
1919
- generate: (req) => this.call("POST", "/v1/images", { body: { ...req, model: req.model ?? "nano_banana" }, timeoutMs: CLIENT_TIMEOUT_MS.image })
1979
+ generate: (req) => this.generateImage(req, callSite(this.images.generate))
1920
1980
  };
1981
+ generateImage(req, site) {
1982
+ return this.call("POST", "/v1/images", { body: { ...req, model: req.model ?? "nano_banana" }, timeoutMs: CLIENT_TIMEOUT_MS.image }).catch((e) => {
1983
+ throw duringCall(e, `images.generate ${req.model ?? "nano_banana"}`, site);
1984
+ });
1985
+ }
1921
1986
  tools = {
1922
1987
  /** The curated LLMOK tool catalog with TON prices. */
1923
1988
  list: async () => {
@@ -1925,23 +1990,26 @@ var Village = class extends Ambasada {
1925
1990
  return Array.isArray(res) ? res : res.tools;
1926
1991
  },
1927
1992
  /** Runs one tool; debits your wallet only on success. Replays with the same idempotency key are free. */
1928
- call: async (id, input, options = {}) => {
1929
- const res = await this.send("POST", `/v1/tools/${encodeURIComponent(id)}`, {
1930
- body: input ?? {},
1931
- retry: true,
1932
- // Image tools take as long as images; a routed search may try several providers within the API's 60 s.
1933
- timeoutMs: id.startsWith("image.") ? CLIENT_TIMEOUT_MS.image : CLIENT_TIMEOUT_MS.tool,
1934
- headers: { "idempotency-key": options.idempotencyKey ?? randomKey() }
1935
- });
1936
- const json = res.json;
1937
- return {
1938
- output: json.output,
1939
- callId: json.call_id ?? res.headers.get("x-village-call-id"),
1940
- amount: json.cost ?? num(res.headers.get("x-village-amount")),
1941
- replayed: res.headers.get("idempotent-replayed") === "true"
1942
- };
1943
- }
1993
+ call: (id, input, options = {}) => this.callTool(id, input, options, callSite(this.tools.call))
1944
1994
  };
1995
+ async callTool(id, input, options, site) {
1996
+ const res = await this.send("POST", `/v1/tools/${encodeURIComponent(id)}`, {
1997
+ body: input ?? {},
1998
+ retry: true,
1999
+ // Image tools take as long as images; a routed search may try several providers within the API's 60 s.
2000
+ timeoutMs: id.startsWith("image.") ? CLIENT_TIMEOUT_MS.image : CLIENT_TIMEOUT_MS.tool,
2001
+ headers: { "idempotency-key": options.idempotencyKey ?? randomKey() }
2002
+ }).catch((e) => {
2003
+ throw duringCall(e, `tools.call ${id}`, site);
2004
+ });
2005
+ const json = res.json;
2006
+ return {
2007
+ output: json.output,
2008
+ callId: json.call_id ?? res.headers.get("x-village-call-id"),
2009
+ amount: json.cost ?? num(res.headers.get("x-village-amount")),
2010
+ replayed: res.headers.get("idempotent-replayed") === "true"
2011
+ };
2012
+ }
1945
2013
  // ── listings, wallet, ledger ──────────────────────────────────────────────
1946
2014
  listings = {
1947
2015
  /** Creates or updates an offering (price changes are versioned). */
@@ -1996,7 +2064,11 @@ var Village = class extends Ambasada {
1996
2064
  body: { decision, ...reason ? { reason } : {}, ...options.wait ? { wait: true } : {} },
1997
2065
  ...options.wait ? { timeoutMs: CHAT_WAIT_MS + 15e3 } : {}
1998
2066
  }),
1999
- transcript: (since = 0, limit = 200) => this.call("GET", "/v1/agent/transcript", { query: { since, limit } }),
2067
+ /**
2068
+ * The agent's transcript after `since`, oldest first: the first `limit` entries, or with `newest` the last `limit`.
2069
+ * `more`: entries after this page (read on from `next_since`); `earlier`: with `newest`, the ones before it.
2070
+ */
2071
+ transcript: (since = 0, limit = 200, options = {}) => this.call("GET", "/v1/agent/transcript", { query: { since, limit, ...options.newest ? { newest: 1 } : {} } }),
2000
2072
  state: () => this.call("GET", "/v1/agent/state")
2001
2073
  };
2002
2074
  };
package/dist/types.d.ts CHANGED
@@ -17,7 +17,7 @@ export interface ReceiptClaims {
17
17
  /** TON you may still spend downstream on behalf of this call. */
18
18
  budget?: number;
19
19
  deal_id?: string;
20
- /** An async offering's call: when the buyer's max_wait_s runs out (unix seconds); `startJob(…, { deadline: call.deadline * 1000 })`. */
20
+ /** An async offering's call: when the buyer's max_wait_s runs out (unix seconds); pass it as it is: `startJob(…, { deadline: call.deadline })` (the templates' jobs take seconds or ms). */
21
21
  deadline?: number;
22
22
  body_sha256: string;
23
23
  iat: number;
@@ -159,7 +159,7 @@ export interface BuyOptions {
159
159
  }
160
160
  /** What an async offering's `GET <endpoint>/jobs/<id>` says (village.jobStatus): the Gateway settles on done or failed. */
161
161
  export type JobAnswer =
162
- /** `attempts`: the failed ones so far (the template's `jobAnswer(job)`); the buyer sees "working (attempt 2, last error: …)". */
162
+ /** `attempts`: the failed ones so far (the template's `jobAnswer(job)`); the buyer sees "working (attempt 1 failed: …; retrying)". */
163
163
  {
164
164
  status: "working";
165
165
  attempts?: {
@@ -192,7 +192,7 @@ export interface BuyResult<Output = any> {
192
192
  job?: string;
193
193
  }
194
194
  /** What the village sells (apps/api/config/pricing.json); any other model is refused. */
195
- export type ChatModel = "deepseek-v4.1-flash" | (string & {});
195
+ export type ChatModel = "deepseek-v4.1-flash" | "glm-5.3-flash" | (string & {});
196
196
  export interface ChatMessage {
197
197
  role: "system" | "user" | "assistant" | "tool";
198
198
  content: string;
@@ -204,8 +204,11 @@ export interface ChatRequest {
204
204
  messages: ChatMessage[];
205
205
  temperature?: number;
206
206
  max_tokens?: number;
207
- /** How much the model thinks before it answers; the village sends "low" when you leave it out. More thinking takes longer and more of `max_tokens`. */
208
- reasoning_effort?: "low" | "medium" | "high" | (string & {});
207
+ /**
208
+ * deepseek-v4.1-flash reasons only when you send `"on"` (the village sends `"none"` otherwise): it thinks 1–3.5k
209
+ * tokens first, inside `max_tokens`, which is then at least 8192. glm-5.3-flash always reasons.
210
+ */
211
+ reasoning_effort?: "none" | "on" | (string & {});
209
212
  /** `json_schema`: JSON in the schema's shape (live, and the mock's sample of it under `ambasada dev`); `json_object`: any JSON. */
210
213
  response_format?: {
211
214
  type: "text" | "json_object";
@@ -232,7 +235,12 @@ export interface ChatResponse {
232
235
  prompt_tokens: number;
233
236
  completion_tokens: number;
234
237
  total_tokens: number;
238
+ completion_tokens_details?: {
239
+ reasoning_tokens?: number;
240
+ };
235
241
  };
242
+ /** On an empty answer cut by `max_tokens` (`finish_reason: "length"`): what to change. */
243
+ village_hint?: string;
236
244
  [key: string]: unknown;
237
245
  }
238
246
  export interface ImageRequest {
@@ -336,6 +344,8 @@ export interface AgentSettings {
336
344
  };
337
345
  /** Agent tools offered to the model (default: all). */
338
346
  tools?: string[];
347
+ /** A wake changes one offering's price at most this often, in minutes (default 120; 0: any time). */
348
+ price_cooldown_minutes?: number;
339
349
  /**
340
350
  * The whole company's spend limits, TON: the product's own LLM and image calls count too, not only the agent's
341
351
  * purchases. Over `auto_per_payment` (and `auto_per_day`) an agent payment needs the team's approval.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ambasada/village",
3
- "version": "0.8.9",
3
+ "version": "0.8.11",
4
4
  "description": "The Hack A Ton add-on for Ambasada apps: TON checkout, receipts, offerings, LLM, the agent, the market; CLI commands and the local bank.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,15 +27,15 @@
27
27
  "node": ">=22"
28
28
  },
29
29
  "dependencies": {
30
- "@ambasada/sdk": "^0.8.9"
30
+ "@ambasada/sdk": "^0.8.11"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^22.20.5",
34
34
  "esbuild": "^0.28.2",
35
35
  "typescript": "~5.9.3",
36
36
  "vitest": "^4.1.12",
37
- "@ambasada/protocol": "0.1.0",
38
- "@ambasada/cli": "0.8.9"
37
+ "@ambasada/cli": "0.8.11",
38
+ "@ambasada/protocol": "0.1.0"
39
39
  },
40
40
  "author": "CENTYSO (https://centyso.com)",
41
41
  "homepage": "https://ambasada.id",