opencrowd 0.6.0 → 0.6.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.
Files changed (2) hide show
  1. package/bundle/opencrowd.js +202 -43
  2. package/package.json +1 -1
@@ -16114,6 +16114,41 @@ var init_spending = __esm({
16114
16114
  }
16115
16115
  });
16116
16116
 
16117
+ // ../../packages/protocol/dist/financial-state.js
16118
+ var usdc, FinancialStateSchema;
16119
+ var init_financial_state = __esm({
16120
+ "../../packages/protocol/dist/financial-state.js"() {
16121
+ "use strict";
16122
+ init_zod();
16123
+ usdc = external_exports.string().regex(/^\d+\.\d{6}$/);
16124
+ FinancialStateSchema = external_exports.discriminatedUnion("status", [
16125
+ external_exports.object({
16126
+ status: external_exports.literal("available"),
16127
+ run_id: external_exports.string().uuid(),
16128
+ observed_at: external_exports.string().datetime(),
16129
+ currency: external_exports.literal("USDC"),
16130
+ wallet: external_exports.object({
16131
+ balance_status: external_exports.enum(["available", "unavailable"]),
16132
+ balance_usdc: usdc.nullable(),
16133
+ reserved_usdc: usdc,
16134
+ available_usdc: usdc.nullable()
16135
+ }),
16136
+ current_run: external_exports.object({
16137
+ inference_spent_usdc: usdc,
16138
+ service_spent_usdc: usdc,
16139
+ total_spent_usdc: usdc,
16140
+ reserved_usdc: usdc,
16141
+ budget_limit_usdc: usdc,
16142
+ remaining_budget_usdc: usdc,
16143
+ per_call_approval_threshold_usdc: usdc,
16144
+ approval_mode: external_exports.enum(["auto", "ask", "off"])
16145
+ })
16146
+ }),
16147
+ external_exports.object({ status: external_exports.literal("unavailable"), reason: external_exports.string() })
16148
+ ]);
16149
+ }
16150
+ });
16151
+
16117
16152
  // ../../packages/protocol/dist/index.js
16118
16153
  var init_dist = __esm({
16119
16154
  "../../packages/protocol/dist/index.js"() {
@@ -16129,6 +16164,7 @@ var init_dist = __esm({
16129
16164
  init_codec();
16130
16165
  init_context();
16131
16166
  init_spending();
16167
+ init_financial_state();
16132
16168
  }
16133
16169
  });
16134
16170
 
@@ -34182,6 +34218,7 @@ var init_gateway = __esm({
34182
34218
  if (outcome === "unknown") {
34183
34219
  const missingReceipt = result.ambiguityReason === "missing_receipt";
34184
34220
  const unsupportedReceipt = result.ambiguityReason === "unsupported_receipt";
34221
+ const timedOut = result.ambiguityReason === "timeout";
34185
34222
  return {
34186
34223
  ok: false,
34187
34224
  error: missingReceipt ? [
@@ -34190,6 +34227,9 @@ var init_gateway = __esm({
34190
34227
  ].join(" ") : unsupportedReceipt ? [
34191
34228
  `the call to ${endpoint} returned a settlement receipt on an unsupported payment rail (recorded as purchase ${purchaseId}).`,
34192
34229
  "It was NOT retried and must not be retried automatically; reconcile the payment manually before continuing."
34230
+ ].join(" ") : timedOut ? [
34231
+ `the call to ${endpoint} did not answer within the wait limit and its payment state is unknown (recorded as purchase ${purchaseId}).`,
34232
+ "The stored purchase was checked once without a final result. It was NOT retried and must not be retried automatically; the owner can reconcile it from the payment ledger."
34193
34233
  ].join(" ") : [
34194
34234
  `the call to ${endpoint} failed in transport and its payment state is unknown (recorded as purchase ${purchaseId}).`,
34195
34235
  "It was NOT retried and must not be retried automatically; verify the service state before calling again."
@@ -80049,12 +80089,15 @@ function economyTools(economy, options = {}) {
80049
80089
  execute: async (name, args, operationId) => {
80050
80090
  if (options.resuming && name === "call_paid_service")
80051
80091
  await economy.execute("inspect_paid_service", { url: args.url, method: args.method, sample_body: args.body });
80052
- return economy.execute(name, args, operationId);
80092
+ const result = await economy.execute(name, args, operationId);
80093
+ if (name === "request_service" && result.ok)
80094
+ reflected = true;
80095
+ return result;
80053
80096
  },
80054
- completionGate: async () => {
80097
+ completionGate: async (alreadyNudged = false) => {
80055
80098
  const pending = await economy.hasPendingRequiredReviews();
80056
80099
  const review = pending ? "A paid purchase still needs its required review; submit it with review_paid_service." : "";
80057
- if (reflected || !supportsReflection)
80100
+ if (reflected || alreadyNudged || !supportsReflection)
80058
80101
  return review || void 0;
80059
80102
  reflected = true;
80060
80103
  const status = await economy.execute("crowdcode_status", {});
@@ -80174,7 +80217,7 @@ import { isAbsolute as isAbsolute3 } from "node:path";
80174
80217
  function createHostedProvider(options) {
80175
80218
  if (!isAbsolute3(options.socketPath))
80176
80219
  throw new Error("Hosted bridge socket must be absolute");
80177
- const tools = [...OPEN_CROWD_TOOLS.filter((tool) => !["spawn_subagent", "check_subagents"].includes(tool.name)), ...options.extraTools ?? []];
80220
+ const tools = [...OPEN_CROWD_TOOLS.filter((tool) => !["spawn_subagent", "check_subagents"].includes(tool.name)).map((tool) => tool.name === "get_budget_status" ? { ...tool, description: "Read authoritative hosted finances: live USDC wallet balance, settled inference and service spending, pending holds, and remaining authorized budget for this run." } : tool), ...options.extraTools ?? []];
80178
80221
  return {
80179
80222
  async complete(messages, context) {
80180
80223
  const operationId = context?.operationId;
@@ -80254,6 +80297,16 @@ function rejectedRequest(status, text) {
80254
80297
  return void 0;
80255
80298
  }
80256
80299
  }
80300
+ async function readHostedFinancialState(options) {
80301
+ try {
80302
+ const reply = await postHostedTool(options, "economy.balance", {}, { timeoutMs: 1e4 });
80303
+ const parsed = FinancialStateSchema.safeParse(reply.data?.financial_state);
80304
+ if (reply.ok && parsed.success && parsed.data.status === "available" && parsed.data.run_id === options.runId)
80305
+ return parsed.data;
80306
+ } catch {
80307
+ }
80308
+ return { status: "unavailable", reason: "Hosted financial state could not be read. Do not infer balance or spending from the local session budget." };
80309
+ }
80257
80310
  async function postHostedTool(options, name, args, settings = {}) {
80258
80311
  if (!isAbsolute3(options.socketPath))
80259
80312
  throw new Error("Hosted bridge socket must be absolute");
@@ -80288,7 +80341,8 @@ async function postHostedTool(options, name, args, settings = {}) {
80288
80341
  }
80289
80342
  });
80290
80343
  });
80291
- req.setTimeout(settings.timeoutMs ?? SERVICE_BRIDGE_TIMEOUT_MS, () => req.destroy(new Error("Hosted tool timed out")));
80344
+ const timeoutMs = settings.timeoutMs ?? SERVICE_BRIDGE_TIMEOUT_MS;
80345
+ req.setTimeout(timeoutMs, () => req.destroy(new HostedToolTimeout(name, timeoutMs)));
80292
80346
  req.on("error", reject);
80293
80347
  req.end(body);
80294
80348
  });
@@ -80322,7 +80376,7 @@ function createHostedToolExecutor(options) {
80322
80376
  }
80323
80377
  };
80324
80378
  }
80325
- var HostedRequestError, HOSTED_TOOL_REQUEST_BYTES;
80379
+ var HostedRequestError, HOSTED_TOOL_REQUEST_BYTES, HostedToolTimeout;
80326
80380
  var init_hosted_provider = __esm({
80327
80381
  "../../packages/agent-runtime/dist/hosted-provider.js"() {
80328
80382
  "use strict";
@@ -80340,6 +80394,15 @@ var init_hosted_provider = __esm({
80340
80394
  }
80341
80395
  };
80342
80396
  HOSTED_TOOL_REQUEST_BYTES = 1024 * 1024;
80397
+ HostedToolTimeout = class extends Error {
80398
+ timeoutMs;
80399
+ code = "hosted_tool_timeout";
80400
+ constructor(name, timeoutMs) {
80401
+ super(`Hosted tool ${name} timed out after ${Math.round(timeoutMs / 1e3)}s`);
80402
+ this.timeoutMs = timeoutMs;
80403
+ this.name = "HostedToolTimeout";
80404
+ }
80405
+ };
80343
80406
  }
80344
80407
  });
80345
80408
 
@@ -80352,7 +80415,7 @@ function createHostedEconomy(options) {
80352
80415
  const directory = new ServiceDirectory(fetcher, (options.crowdcodeBase ?? DEFAULT_CROWDCODE_BASE).replace(/\/+$/, ""));
80353
80416
  return new EconomyGateway({
80354
80417
  session: options.session,
80355
- agentcash: new HostedAgentCashAdapter(directory, socket, fetcher),
80418
+ agentcash: new HostedAgentCashAdapter(directory, socket, fetcher, options.reconcileDelayMs),
80356
80419
  crowdcode: new HostedCrowdCodeAdapter(directory, socket),
80357
80420
  approvalMode: "auto",
80358
80421
  hostedSpending: true,
@@ -80362,7 +80425,8 @@ function createHostedEconomy(options) {
80362
80425
  });
80363
80426
  }
80364
80427
  function hostedDynamicTools(economy) {
80365
- return economyTools(economy, { hidden: HIDDEN_HOSTED_TOOLS });
80428
+ const tools = economyTools(economy, { hidden: HIDDEN_HOSTED_TOOLS });
80429
+ return { ...tools, definitions: tools.definitions.map((tool) => tool.name === "get_wallet_status" ? { ...tool, description: "Read this hosted agent's USDC wallet balance and authoritative current-run inference spending, service spending, payment holds and authorized budget. Read-only." } : tool) };
80366
80430
  }
80367
80431
  function parseListedService(raw) {
80368
80432
  const row = objectValue4(raw);
@@ -80426,7 +80490,7 @@ function numberValue6(value) {
80426
80490
  }
80427
80491
  return void 0;
80428
80492
  }
80429
- var DEFAULT_CROWDCODE_BASE, LIST_CACHE_MS, PAY_TIMEOUT_MS, FREE_FETCH_TIMEOUT_MS, USDC_ATOMIC_PER_USD, HIDDEN_HOSTED_TOOLS, ServiceDirectory, HostedAgentCashAdapter, HostedCrowdCodeAdapter;
80493
+ var DEFAULT_CROWDCODE_BASE, LIST_CACHE_MS, PAY_TIMEOUT_MS, RECONCILE_DELAY_MS, NOT_SENT_CODES, FREE_FETCH_TIMEOUT_MS, USDC_ATOMIC_PER_USD, HIDDEN_HOSTED_TOOLS, ServiceDirectory, HostedAgentCashAdapter, HostedCrowdCodeAdapter;
80430
80494
  var init_hosted_economy = __esm({
80431
80495
  "../../packages/agent-runtime/dist/hosted-economy.js"() {
80432
80496
  "use strict";
@@ -80434,14 +80498,17 @@ var init_hosted_economy = __esm({
80434
80498
  init_worker();
80435
80499
  init_dist2();
80436
80500
  init_dist4();
80501
+ init_dist();
80437
80502
  init_hosted_provider();
80438
80503
  init_economy_tools();
80439
80504
  DEFAULT_CROWDCODE_BASE = "https://crowdcode-backend.onrender.com";
80440
80505
  LIST_CACHE_MS = 6e4;
80441
- PAY_TIMEOUT_MS = 12e4;
80506
+ PAY_TIMEOUT_MS = SERVICE_GATEWAY_TIMEOUT_MS + 3e4;
80507
+ RECONCILE_DELAY_MS = 2e3;
80508
+ NOT_SENT_CODES = /* @__PURE__ */ new Set(["ENOENT", "ECONNREFUSED", "EACCES", "ENOTDIR"]);
80442
80509
  FREE_FETCH_TIMEOUT_MS = 3e4;
80443
80510
  USDC_ATOMIC_PER_USD = 1e6;
80444
- HIDDEN_HOSTED_TOOLS = /* @__PURE__ */ new Set(["bridge_usdc", "get_wallet_status"]);
80511
+ HIDDEN_HOSTED_TOOLS = /* @__PURE__ */ new Set(["bridge_usdc"]);
80445
80512
  ServiceDirectory = class {
80446
80513
  fetcher;
80447
80514
  base;
@@ -80488,12 +80555,14 @@ var init_hosted_economy = __esm({
80488
80555
  directory;
80489
80556
  socket;
80490
80557
  fetcher;
80558
+ reconcileDelayMs;
80491
80559
  /** Rails observed by inspection; `fetch` refuses anything but Base x402 regardless of the caller's rail. */
80492
80560
  inspectedRails = /* @__PURE__ */ new Map();
80493
- constructor(directory, socket, fetcher) {
80561
+ constructor(directory, socket, fetcher, reconcileDelayMs = RECONCILE_DELAY_MS) {
80494
80562
  this.directory = directory;
80495
80563
  this.socket = socket;
80496
80564
  this.fetcher = fetcher;
80565
+ this.reconcileDelayMs = reconcileDelayMs;
80497
80566
  this.discovery = new BaseServiceDiscovery(async (query, options) => {
80498
80567
  const reply = await postHostedTool(this.socket, "economy.read", {
80499
80568
  url: "https://agentcash.dev/api/search",
@@ -80549,24 +80618,31 @@ var init_hosted_economy = __esm({
80549
80618
  const body = request2.body === void 0 ? void 0 : typeof request2.body === "string" ? request2.body : JSON.stringify(request2.body);
80550
80619
  const hash5 = request2.operationId ? createHash4("sha256").update(`${this.socket.runId}:${request2.operationId}`).digest("hex") : void 0;
80551
80620
  const purchaseId = hash5 ? `${hash5.slice(0, 8)}-${hash5.slice(8, 12)}-4${hash5.slice(13, 16)}-8${hash5.slice(17, 20)}-${hash5.slice(20, 32)}` : randomUUID7();
80552
- let reply;
80621
+ const payArgs = {
80622
+ purchase_request_id: purchaseId,
80623
+ service_id: serviceId,
80624
+ url: request2.url,
80625
+ method,
80626
+ body,
80627
+ max_cost_cents: Math.ceil(request2.maxAmountUsd * 100)
80628
+ };
80629
+ const timeoutMs = Math.min(request2.timeoutMs ?? PAY_TIMEOUT_MS, PAY_TIMEOUT_MS);
80630
+ let reply, timedOut = false;
80553
80631
  try {
80554
- reply = await postHostedTool(this.socket, "economy.pay", {
80555
- purchase_request_id: purchaseId,
80556
- service_id: serviceId,
80557
- url: request2.url,
80558
- method,
80559
- body,
80560
- max_cost_cents: Math.ceil(request2.maxAmountUsd * 100)
80561
- }, { timeoutMs: request2.timeoutMs ?? PAY_TIMEOUT_MS });
80632
+ reply = await postHostedTool(this.socket, "economy.pay", payArgs, { timeoutMs });
80562
80633
  } catch (error53) {
80563
- return {
80564
- ok: false,
80565
- ambiguous: true,
80566
- ambiguityReason: "transport",
80567
- data: void 0,
80568
- error: `payment request to the supervisor failed in transport: ${error53.message}`
80569
- };
80634
+ timedOut = error53 instanceof HostedToolTimeout;
80635
+ const recovered = await this.reconcile(error53, payArgs, timeoutMs);
80636
+ if (!recovered) {
80637
+ return {
80638
+ ok: false,
80639
+ ambiguous: true,
80640
+ ambiguityReason: timedOut ? "timeout" : "transport",
80641
+ data: void 0,
80642
+ error: timedOut ? `the paid call did not answer within ${Math.round(timeoutMs / 1e3)}s and its stored purchase has no final result yet` : `payment request to the supervisor failed in transport: ${error53.message}`
80643
+ };
80644
+ }
80645
+ reply = recovered;
80570
80646
  }
80571
80647
  if (reply.ok) {
80572
80648
  const data = objectValue4(reply.data) ?? {};
@@ -80599,13 +80675,29 @@ var init_hosted_economy = __esm({
80599
80675
  return {
80600
80676
  ok: false,
80601
80677
  ambiguous: true,
80602
- ambiguityReason: "transport",
80678
+ ambiguityReason: timedOut ? "timeout" : "transport",
80603
80679
  data: void 0,
80604
- error: reply.error ?? "the supervisor could not determine whether the payment settled"
80680
+ error: timedOut ? `the paid call did not answer within ${Math.round(timeoutMs / 1e3)}s and its stored purchase has no final result yet` : reply.error ?? "the supervisor could not determine whether the payment settled"
80605
80681
  };
80606
80682
  }
80607
80683
  return refuse(reply.code ? `${reply.error ?? "payment refused"} (${reply.code})` : reply.error ?? "payment refused");
80608
80684
  }
80685
+ /**
80686
+ * Re-post the same purchase once. A finished attempt replays from the ledger;
80687
+ * an attempt still in flight answers payment_unknown; a request that never
80688
+ * reached the supervisor is not reconciled because nothing was started.
80689
+ */
80690
+ async reconcile(error53, payArgs, timeoutMs) {
80691
+ const code = error53?.code;
80692
+ if (typeof code === "string" && NOT_SENT_CODES.has(code))
80693
+ return void 0;
80694
+ await new Promise((resolve9) => setTimeout(resolve9, this.reconcileDelayMs));
80695
+ try {
80696
+ return await postHostedTool(this.socket, "economy.pay", payArgs, { timeoutMs });
80697
+ } catch {
80698
+ return void 0;
80699
+ }
80700
+ }
80609
80701
  /** The supervisor found no 402: the endpoint is free, so the worker fetches it itself. */
80610
80702
  async fetchUnpaid(url3, method, body, headers) {
80611
80703
  try {
@@ -80948,6 +81040,15 @@ var init_worker = __esm({
80948
81040
  const hosted = this.options.hostedTools?.(run.start, session);
80949
81041
  const economy = this.options.economy?.(run.start, session);
80950
81042
  const dynamicTools = economy ? hostedDynamicTools(economy) : void 0;
81043
+ let latestFinancialState = { status: "unavailable", reason: "Hosted financial state has not been read." };
81044
+ const financialState = this.options.financialState ? async () => {
81045
+ try {
81046
+ latestFinancialState = await this.options.financialState(run.start, session);
81047
+ } catch {
81048
+ latestFinancialState = { status: "unavailable", reason: "Hosted financial state could not be read; do not use local budget figures." };
81049
+ }
81050
+ return latestFinancialState;
81051
+ } : void 0;
80951
81052
  const record3 = async (name, args, invoke) => {
80952
81053
  const call2 = run.checkpoint?.response?.toolCalls.find((c) => c.name === name && !run.checkpoint?.completedTools[c.id]);
80953
81054
  const id = `${run.checkpoint?.turn ?? 0}:${call2?.id ?? randomUUID8()}`;
@@ -80971,6 +81072,10 @@ var init_worker = __esm({
80971
81072
  if (saved?.approvalPending && name === "call_paid_service" && economy)
80972
81073
  await economy.execute("inspect_paid_service", { url: args.url, method: args.method, sample_body: args.body });
80973
81074
  output3 = await invoke();
81075
+ if (financialState && name === "get_budget_status")
81076
+ output3 = { ok: true, data: latestFinancialState };
81077
+ if (financialState && name === "complete_session" && output3.ok && output3.data && typeof output3.data === "object")
81078
+ output3 = { ...output3, data: { ...output3.data, budget: latestFinancialState } };
80974
81079
  } catch (error53) {
80975
81080
  if (error53 instanceof RuntimePause && error53.outcome === "waiting_for_approval")
80976
81081
  await this.mutate(() => {
@@ -81066,10 +81171,11 @@ var init_worker = __esm({
81066
81171
  contextWindowTokens: run.start.payload.modelPolicy.contextWindowTokens,
81067
81172
  maxOutputTokens: run.start.payload.modelPolicy.maxOutputTokens,
81068
81173
  provider,
81174
+ financialState,
81069
81175
  history: run.checkpoint ? void 0 : await this.history(session),
81070
81176
  ...dynamicTools ? {
81071
81177
  dynamicTools: { definitions: dynamicTools.definitions, execute: (name, args) => record3(name, args, () => dynamicTools.execute(name, args, `${run.checkpoint?.turn ?? 0}:${run.checkpoint?.response?.toolCalls.find((c) => c.name === name && !run.checkpoint?.completedTools[c.id])?.id}`)) },
81072
- completionGate: () => dynamicTools.completionGate(),
81178
+ completionGate: (alreadyNudged) => dynamicTools.completionGate(alreadyNudged),
81073
81179
  knowledge: await this.knowledgeOption(session)
81074
81180
  } : {},
81075
81181
  onCheckpoint: async (checkpoint) => {
@@ -81392,6 +81498,8 @@ async function runAgentTaskDetailed(session, task, options = {}) {
81392
81498
  "Try to solve the user's task completely. Use local files and bash when they are sufficient, but remember they are bounded by the user's installed tools, credentials, network, open ports, and process lifetime.",
81393
81499
  "Before repeatedly fighting the local environment, decide whether the task needs external capability such as hosting, persistent infrastructure, remote compute, specialized APIs, live data, or access the user's device does not have."
81394
81500
  ];
81501
+ if (options.hosted)
81502
+ systemPromptParts.push("You operate this service for its owner. Inference and paid tools debit its USDC wallet. When authorized to build or sell a service, take concrete steps within the owner's configured scope and budget. Ask only for missing information or permissions that materially block progress. Report actual costs and outcomes. Financial sustainability is the owner's business objective; respect their stop controls.", "Financial state: the current hosted financial snapshot is authoritative; historical local session budget figures are not wallet balances and may omit inference spending. Wallet balance, pending payment holds, settled inference spend, settled service spend, and remaining run budget are separate. Spending totals cover only the identified current run. A budget limit does not supply funds. An unavailable balance is unknown, not zero. The snapshot before a model call excludes that call's eventual charge; never claim a response was free or that you spent nothing merely because no paid tool ran. Use get_budget_status or get_wallet_status for refreshed financial state. Respect approval mode and per-call limits; over-limit requests require the owner's approval.");
81395
81503
  if (options.dynamicTools) {
81396
81504
  systemPromptParts.push("When the local computer is not the right environment, or after one clear local capability failure, buy external capability: find_paid_service to discover, inspect_paid_service to see the exact schema/price/reputation, call_paid_service to execute through the enforced purchase lifecycle, and review_paid_service for the required review after every confirmed paid call (success or failure). The same review tool accepts unpaid experiences; review relevant free results or failed attempts without inventing payment evidence. Distinguish provider faults from local errors and uncertain failures.", ...options.hosted ? ["Discover services through AgentCash search and provider catalogs. You can use new services without prior CrowdCode reviews. Only x402 USDC on Base is payable, within the user's configured limits; over-limit quotes request approval. Use read_service for authenticated job polling and never resubmit a pending generation job."] : [], capabilityIndex ?? DEFAULT_CAPABILITY_INDEX, "When a task needs current web facts, search results, or unfamiliar page content, make one paid web search your FIRST move \u2014 do not serially guess URLs with curl; one paid search replaces minutes of blind fetching and costs less than the LLM turns it saves.", "Approval, budget, reputation, and payment rails are enforced in code \u2014 you cannot bypass them, so state costs plainly and never invent payment details.", "When the user says CrowdCode on/off, call set_crowdcode_enabled; default to session scope unless they ask for a persistent default. Use list_my_reviews and delete_my_review for user-directed review management, including while off. Before finishing a substantive task, consider concrete paid services that would fix real shortcomings and submit qualifying demand with request_service, unless CrowdCode is off. Never invent a purchase or demand.", "Each tool result includes the budget before and after that tool call. Never ask for wallet private keys or secrets.", ...options.promptSections ?? []);
81397
81505
  } else {
@@ -81413,11 +81521,24 @@ async function runAgentTaskDetailed(session, task, options = {}) {
81413
81521
  ...options.history ?? [],
81414
81522
  { role: "user", content: task }
81415
81523
  ];
81524
+ let financial = { status: "unavailable", reason: "Hosted financial state has not been read." };
81525
+ const refreshFinancialState = async () => {
81526
+ if (!options.financialState)
81527
+ return;
81528
+ try {
81529
+ financial = FinancialStateSchema.parse(await options.financialState());
81530
+ } catch {
81531
+ financial = { status: "unavailable", reason: "Hosted financial state could not be read; do not use local budget figures." };
81532
+ }
81533
+ };
81534
+ const currentBudget = () => options.financialState ? financial : budgetStatus(session);
81535
+ const financialPrefix = "Current hosted financial snapshot: ";
81416
81536
  if (!options.resume)
81417
81537
  await options.onMessage?.({ role: "user", content: task });
81418
81538
  const maxTurns = options.maxTurns ?? 100;
81419
81539
  const repeatedFailures = new Map(options.resume?.repeatedFailures);
81420
81540
  let completionNudges = options.resume?.completionNudges ?? 0;
81541
+ let pendingFinalMessage = options.resume?.pendingFinalMessage;
81421
81542
  let outputContinuationNudges = options.resume?.outputContinuationNudges ?? 0;
81422
81543
  let outputContinuationPrefix = options.resume?.outputContinuationPrefix ?? "";
81423
81544
  let serviceCallFailures = options.resume?.serviceCallFailures ?? 0;
@@ -81431,6 +81552,7 @@ async function runAgentTaskDetailed(session, task, options = {}) {
81431
81552
  context: contextState,
81432
81553
  deliveredMessageIds: [...deliveredMessageIds],
81433
81554
  completionNudges,
81555
+ pendingFinalMessage,
81434
81556
  outputContinuationNudges,
81435
81557
  outputContinuationPrefix,
81436
81558
  serviceCallFailures,
@@ -81444,8 +81566,11 @@ async function runAgentTaskDetailed(session, task, options = {}) {
81444
81566
  messages.push({ role: "user", content: message.prompt });
81445
81567
  deliveredMessageIds.add(message.id);
81446
81568
  }
81447
- if (fresh.length)
81569
+ if (fresh.length) {
81570
+ pendingFinalMessage = void 0;
81571
+ outputContinuationPrefix = "";
81448
81572
  await checkpoint(turn);
81573
+ }
81449
81574
  for (const message of fresh)
81450
81575
  await options.onMessage?.({ role: "user", content: message.prompt });
81451
81576
  if (pending.length)
@@ -81473,6 +81598,15 @@ async function runAgentTaskDetailed(session, task, options = {}) {
81473
81598
  options.onProgress?.({ type: "calling_llm", message: `Calling LLM provider (turn ${turn + 1}/${maxTurns})` });
81474
81599
  let response;
81475
81600
  try {
81601
+ if (options.financialState && !restored?.response && !contextState.prepared) {
81602
+ await refreshFinancialState();
81603
+ const snapshot = { role: "system", content: financialPrefix + JSON.stringify(financial) };
81604
+ const index2 = messages.findIndex((m) => m.role === "system" && m.content.startsWith(financialPrefix));
81605
+ if (index2 === -1)
81606
+ messages.splice(1, 0, snapshot);
81607
+ else
81608
+ messages[index2] = snapshot;
81609
+ }
81476
81610
  response = restored?.response ?? await completeWithContext(session, messages, {
81477
81611
  provider: provider instanceof MockLlmProvider ? { complete: (input2) => completeMockLlmCall(session, provider, input2, turn + 1) } : provider,
81478
81612
  tools: wireToolDefinitions(enabledTools, dynamicDefinitions),
@@ -81489,6 +81623,7 @@ async function runAgentTaskDetailed(session, task, options = {}) {
81489
81623
  }
81490
81624
  });
81491
81625
  options.signal?.throwIfAborted();
81626
+ await refreshFinancialState();
81492
81627
  } catch (error53) {
81493
81628
  if (error53 instanceof BudgetExhaustedError) {
81494
81629
  const summary2 = await completeSession(session, `Stopped: the session budget is exhausted (${error53.message}).`);
@@ -81522,7 +81657,10 @@ ${outputContinuationPrefix}`);
81522
81657
  if (response.toolCalls.length === 0) {
81523
81658
  if (await injectMessages(turn + 1))
81524
81659
  continue;
81525
- const blocker = await options.completionGate?.();
81660
+ const finalMessage = response.content.trim() ? outputContinuationPrefix + response.content : pendingFinalMessage ?? outputContinuationPrefix;
81661
+ if (finalMessage.trim())
81662
+ pendingFinalMessage = finalMessage;
81663
+ const blocker = await options.completionGate?.(completionNudges > 0);
81526
81664
  if (blocker) {
81527
81665
  if (completionNudges >= 1) {
81528
81666
  const summary3 = await completeSession(session, `Stopped: ${blocker}`);
@@ -81532,9 +81670,12 @@ ${outputContinuationPrefix}`);
81532
81670
  const nudge = { role: "user", content: `You cannot finish yet: ${blocker}` };
81533
81671
  messages.push(nudge);
81534
81672
  await options.onMessage?.(nudge);
81673
+ await checkpoint(turn + 1);
81535
81674
  continue;
81536
81675
  }
81537
- const summary2 = await completeSession(session, outputContinuationPrefix + (response.content || "Session completed."));
81676
+ if (!pendingFinalMessage?.trim())
81677
+ throw new Error("The model ended without an answer.");
81678
+ const summary2 = await completeSession(session, pendingFinalMessage);
81538
81679
  return { outcome: "completed", summary: summary2, turns: turn + 1 };
81539
81680
  }
81540
81681
  const spawnedThisTurn = /* @__PURE__ */ new Map();
@@ -81577,7 +81718,7 @@ ${outputContinuationPrefix}`);
81577
81718
  message: `Tool call: ${summarizeToolCall(call2)}`,
81578
81719
  data: { tool: call2.name, arguments: call2.arguments }
81579
81720
  });
81580
- const budgetBeforeToolCall = budgetStatus(session);
81721
+ const budgetBeforeToolCall = currentBudget();
81581
81722
  const callArguments = call2.name === "complete_session" && outputContinuationPrefix ? {
81582
81723
  ...call2.arguments,
81583
81724
  final_message: outputContinuationPrefix + (typeof call2.arguments.final_message === "string" ? call2.arguments.final_message : "")
@@ -81615,7 +81756,13 @@ ${outputContinuationPrefix}`);
81615
81756
  } else {
81616
81757
  result = { ok: false, error: `unknown tool: ${call2.name}` };
81617
81758
  }
81618
- const budgetAfterToolCall = budgetStatus(session);
81759
+ await refreshFinancialState();
81760
+ const budgetAfterToolCall = currentBudget();
81761
+ if (options.financialState && call2.name === "get_budget_status")
81762
+ result = { ok: true, data: financial };
81763
+ if (options.financialState && call2.name === "complete_session" && result.ok && result.data && typeof result.data === "object") {
81764
+ result = { ...result, data: { ...result.data, budget: financial } };
81765
+ }
81619
81766
  options.onProgress?.({
81620
81767
  type: "tool_result",
81621
81768
  message: `Tool result: ${summarizeToolResult(call2.name, result)}`,
@@ -81636,6 +81783,8 @@ ${outputContinuationPrefix}`);
81636
81783
  await checkpoint(turn, response, completedTools);
81637
81784
  await options.onMessage?.(toolMessage);
81638
81785
  }
81786
+ if (call2.name === "call_paid_service" && result.ok)
81787
+ serviceCallFailures = 0;
81639
81788
  if (!result.ok) {
81640
81789
  if (call2.name === "call_paid_service") {
81641
81790
  serviceCallFailures += 1;
@@ -81659,7 +81808,12 @@ ${outputContinuationPrefix}`);
81659
81808
  const result = completedTools[call2.id];
81660
81809
  if (await injectMessages(turn + 1))
81661
81810
  continue;
81662
- const blocker = await options.completionGate?.();
81811
+ if (!result.ok)
81812
+ continue;
81813
+ const finalMessage = result.data && typeof result.data === "object" ? result.data.final_message : void 0;
81814
+ if (typeof finalMessage === "string" && finalMessage.trim())
81815
+ pendingFinalMessage = finalMessage;
81816
+ const blocker = await options.completionGate?.(completionNudges > 0);
81663
81817
  if (blocker) {
81664
81818
  if (completionNudges >= 1) {
81665
81819
  const summary3 = await completeSession(session, `Stopped: ${blocker}`);
@@ -81667,23 +81821,25 @@ ${outputContinuationPrefix}`);
81667
81821
  }
81668
81822
  completionNudges += 1;
81669
81823
  const gateMessage = {
81670
- role: "tool",
81671
- toolCallId: call2.id,
81672
- content: JSON.stringify({ result: { ok: false, error: `cannot complete yet: ${blocker}` } })
81824
+ role: "user",
81825
+ content: `You cannot finish yet: ${blocker}`
81673
81826
  };
81674
81827
  messages.push(gateMessage);
81675
81828
  await options.onMessage?.(gateMessage);
81829
+ await checkpoint(turn + 1);
81676
81830
  continue;
81677
81831
  }
81832
+ if (!pendingFinalMessage?.trim())
81833
+ throw new Error("The model ended without an answer.");
81678
81834
  const backgroundResults = backgroundSubagents.size > 0 ? await drainBackground() : void 0;
81679
- if (result.ok && result.data && typeof result.data === "object") {
81680
- const summary3 = { ...result.data };
81835
+ if (result.data && typeof result.data === "object") {
81836
+ const summary3 = { ...result.data, final_message: pendingFinalMessage };
81681
81837
  if (backgroundResults) {
81682
81838
  summary3.background_subagents = backgroundResults;
81683
81839
  }
81684
81840
  return { outcome: "completed", summary: summary3, turns: turn + 1 };
81685
81841
  }
81686
- const summary2 = await completeSession(session, outputContinuationPrefix + (response.content || result.error || "Session completed."));
81842
+ const summary2 = await completeSession(session, pendingFinalMessage);
81687
81843
  if (backgroundResults) {
81688
81844
  summary2.background_subagents = backgroundResults;
81689
81845
  }
@@ -82175,6 +82331,8 @@ function summarizeToolResult(name, result) {
82175
82331
  return `call_paid_service ${String(data.outcome ?? "?")} HTTP ${String(data.status ?? "?")}, charged ${formatCents(Number(data.charged_cost_cents ?? 0))}${data.artifact_path ? `, saved ${String(data.artifact_path)}` : ""}`;
82176
82332
  }
82177
82333
  if (name === "get_budget_status" && result.data && typeof result.data === "object") {
82334
+ if ("status" in result.data)
82335
+ return `hosted finances: ${String(result.data.status)}`;
82178
82336
  const data = result.data;
82179
82337
  return `remaining ${formatCents(Number(data.remaining_cents ?? 0))}`;
82180
82338
  }
@@ -82629,6 +82787,7 @@ async function workerCommand(args) {
82629
82787
  provider: (run, session, extraTools) => socketPath ? createHostedProvider({ socketPath, runId: run.runId, sessionId: session.sessionId, extraTools }) : createWorkerDemoProvider(),
82630
82788
  hostedTools: socketPath ? (run, session) => createHostedToolExecutor({ socketPath, runId: run.runId, sessionId: session.sessionId }) : void 0,
82631
82789
  economy: socketPath ? (run, session) => createHostedEconomy({ socketPath, runId: run.runId, sessionId: session.sessionId, session }) : void 0,
82790
+ financialState: socketPath ? (run, session) => readHostedFinancialState({ socketPath, runId: run.runId, sessionId: session.sessionId }) : void 0,
82632
82791
  output: (line) => {
82633
82792
  process.stdout.write(line);
82634
82793
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencrowd",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "A CLI agent with its own wallet — discovers x402 services, pays in USDC, and completes tasks under a local budget",
5
5
  "type": "module",
6
6
  "license": "MIT",