@gr8ful/spf 0.19.0 → 0.19.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/README.md +8 -0
- package/assets/skill/references/config.md +1 -0
- package/dist/cli/commands/doctor.js +25 -3
- package/dist/cli/commands/estimate.d.ts +22 -6
- package/dist/cli/commands/estimate.js +32 -10
- package/dist/cli/commands/loop.d.ts +20 -0
- package/dist/cli/commands/loop.js +20 -1
- package/dist/cli/commands/ui.js +2 -1
- package/dist/cli/commands/watch.js +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/interview.js +13 -0
- package/dist/cli/ui/run_dashboard.js +13 -7
- package/dist/core/agent_cc.d.ts +19 -3
- package/dist/core/agent_cc.js +38 -18
- package/dist/core/agent_flue.js +51 -14
- package/dist/core/agent_opencode.d.ts +62 -25
- package/dist/core/agent_opencode.js +71 -30
- package/dist/core/agents.d.ts +51 -4
- package/dist/core/agents.js +79 -4
- package/dist/core/console.d.ts +24 -4
- package/dist/core/console.js +20 -7
- package/dist/core/data_types.d.ts +300 -19
- package/dist/core/data_types.js +134 -5
- package/dist/core/issues/jira_provider.d.ts +51 -1
- package/dist/core/issues/jira_provider.js +69 -1
- package/dist/core/issues/provider.d.ts +23 -0
- package/dist/core/loop.d.ts +39 -1
- package/dist/core/loop.js +33 -2
- package/dist/core/ollama_provider.d.ts +96 -13
- package/dist/core/ollama_provider.js +172 -26
- package/dist/core/otel.js +10 -1
- package/dist/core/otel_propagation.d.ts +168 -24
- package/dist/core/otel_propagation.js +219 -43
- package/dist/core/permissions.d.ts +16 -1
- package/dist/core/permissions.js +91 -3
- package/dist/core/providers.js +8 -3
- package/dist/core/refine.js +13 -1
- package/dist/core/runner.d.ts +33 -2
- package/dist/core/runner.js +40 -5
- package/dist/core/tiering.js +7 -3
- package/dist/core/tracer.d.ts +7 -1
- package/dist/core/tracer.js +15 -3
- package/dist/ui/server/db.d.ts +8 -1
- package/dist/ui/server/db.js +21 -4
- package/dist/ui/server/serve.d.ts +7 -0
- package/dist/ui/server/serve.js +10 -7
- package/dist/ui/shared/types.d.ts +16 -0
- package/package.json +1 -1
package/dist/core/agent_flue.js
CHANGED
|
@@ -317,8 +317,18 @@ const SfAgent = Object.assign(sfAgentRender, {
|
|
|
317
317
|
});
|
|
318
318
|
const pendingUsage = new Map();
|
|
319
319
|
let runtimePromise = null;
|
|
320
|
-
|
|
320
|
+
/**
|
|
321
|
+
* `requestTimeoutMs` (from `defaults.request_timeout_ms`) only has an effect
|
|
322
|
+
* on the FIRST call — `SfAgent.durability` is a static Flue reads once, and
|
|
323
|
+
* `runtimePromise` below already memoizes `start()` to run once per process.
|
|
324
|
+
* A later call with a different value is silently ignored, same as `start()`
|
|
325
|
+
* itself already is; every dispatch in one `spf` process shares one config
|
|
326
|
+
* anyway, so this can't happen in practice outside a test harness.
|
|
327
|
+
*/
|
|
328
|
+
function ensureRuntime(flueDbPath, requestTimeoutMs) {
|
|
321
329
|
if (!runtimePromise) {
|
|
330
|
+
if (requestTimeoutMs !== undefined)
|
|
331
|
+
SfAgent.durability = { timeoutMs: requestTimeoutMs };
|
|
322
332
|
runtimePromise = start({ agents: [SfAgent], db: sqlite(flueDbPath) }).then((flue) => {
|
|
323
333
|
observe((event) => {
|
|
324
334
|
if (event.type !== "turn" || !event.submissionId)
|
|
@@ -394,8 +404,35 @@ export async function run(request, onEvent, onSpawn, onExit) {
|
|
|
394
404
|
// imports this module for `resolveModel()` alone (doctor.ts, interview.ts)
|
|
395
405
|
// without ever dispatching an ollama call.
|
|
396
406
|
const [provider, modelId] = resolveModel(request.model);
|
|
407
|
+
// Outbound OTel propagation (SPF's otel-sdk extension) — see
|
|
408
|
+
// `otel_propagation.ts`'s own header for what this does and does not
|
|
409
|
+
// guarantee. `request.otel` is set only when `observability.otel` is
|
|
410
|
+
// configured for this run (see `agents.ts`'s `send()`); the installer is
|
|
411
|
+
// itself a no-op on `undefined` AND idempotent across every later call in
|
|
412
|
+
// this same process, so this costs nothing for a repo that hasn't
|
|
413
|
+
// configured otel and installs at most once for one that has. Called
|
|
414
|
+
// BEFORE `registerOllamaModel` below (not after, as an earlier version had
|
|
415
|
+
// it): `ollama_provider.ts`'s `modelFor()`/`resolve()` both consult
|
|
416
|
+
// `isFluePropagationInstalled()` to decide whether THEY need to supply
|
|
417
|
+
// `traceparent`/`x-correlation-id`/`x-spf-agent` themselves — if
|
|
418
|
+
// installation happened AFTER the first registration in a run that has
|
|
419
|
+
// otel configured, that first model would be built (and cached in
|
|
420
|
+
// `Model.headers`) believing propagation wasn't installed yet, then keep
|
|
421
|
+
// stale static headers alongside the instrumentation's own per-request
|
|
422
|
+
// ones for the rest of the process (a duplicate-header bug, same shape as
|
|
423
|
+
// BLOCKER A). Installing first means every registration in this run sees
|
|
424
|
+
// the SAME, final installed-state.
|
|
425
|
+
installFluePropagation(request.otel);
|
|
426
|
+
// `adw_id`/`agent_name` (when the caller supplied them — see
|
|
427
|
+
// `data_types.ts`'s `AgentRequest` doc) become this model id's static
|
|
428
|
+
// `x-correlation-id`/`x-spf-agent` gateway headers, re-stamped on every
|
|
429
|
+
// call — see `ollama_provider.ts`'s "Gateway headers" section and
|
|
430
|
+
// `registerOllamaModel`'s own doc (MAJOR-D) for why this must run every
|
|
431
|
+
// time, not just on first registration, and why it's a no-op for these
|
|
432
|
+
// two headers specifically once otel propagation is installed (see
|
|
433
|
+
// `registerFlueSessionTrace` below instead, in that case).
|
|
397
434
|
if (provider === "ollama")
|
|
398
|
-
await registerOllamaModel(modelId);
|
|
435
|
+
await registerOllamaModel(modelId, { adwId: request.adw_id, agentName: request.agent_name });
|
|
399
436
|
// Cloudflare Workers AI is the same self-registration shape as Ollama
|
|
400
437
|
// (no pi-ai/Flue built-in "cloudflare" provider on Node) — see
|
|
401
438
|
// cloudflare_provider.ts's header comment for the Workers AI OpenAI-
|
|
@@ -403,15 +440,7 @@ export async function run(request, onEvent, onSpawn, onExit) {
|
|
|
403
440
|
// real-Bearer-token (not dummy-key) auth.
|
|
404
441
|
if (provider === "cloudflare")
|
|
405
442
|
await registerCloudflareModel(modelId);
|
|
406
|
-
|
|
407
|
-
// `otel_propagation.ts`'s own header for what this does and does not
|
|
408
|
-
// guarantee. `request.otel` is set only when `observability.otel` is
|
|
409
|
-
// configured for this run (see `agents.ts`'s `send()`); the installer is
|
|
410
|
-
// itself a no-op on `undefined` AND idempotent across every later call in
|
|
411
|
-
// this same process, so this costs nothing for a repo that hasn't
|
|
412
|
-
// configured otel and installs at most once for one that has.
|
|
413
|
-
installFluePropagation(request.otel);
|
|
414
|
-
await ensureRuntime(request.flue_db_path);
|
|
443
|
+
await ensureRuntime(request.flue_db_path, request.request_timeout_ms);
|
|
415
444
|
REGISTRY.set(request.session_id, {
|
|
416
445
|
model: request.model,
|
|
417
446
|
thinking: request.thinking,
|
|
@@ -436,9 +465,17 @@ export async function run(request, onEvent, onSpawn, onExit) {
|
|
|
436
465
|
// async context is captured by the first dispatch that started it, so a
|
|
437
466
|
// dispatch-time context wrap would silently mis-attribute every later
|
|
438
467
|
// agent's spans into the FIRST agent's trace. No-op when otel is
|
|
439
|
-
// unconfigured.
|
|
440
|
-
|
|
441
|
-
|
|
468
|
+
// unconfigured. The SAME registration also carries `adw_id`/`agent_name`
|
|
469
|
+
// (MAJOR-D) — `otel_propagation.ts`'s `GatewayHeadersPropagator` reads
|
|
470
|
+
// them back per real outbound call this session makes, exactly the way
|
|
471
|
+
// `resolveFlueRootContext` already reads `traceparent` back per span.
|
|
472
|
+
if (request.otel) {
|
|
473
|
+
registerFlueSessionTrace(request.session_id, {
|
|
474
|
+
traceparent: request.otel.traceparent,
|
|
475
|
+
adwId: request.adw_id,
|
|
476
|
+
agentName: request.agent_name,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
442
479
|
const receipt = await handle.dispatch(request.prompt);
|
|
443
480
|
const slot = { usage: new UsageBreakdown(), context_tokens: 0 };
|
|
444
481
|
pendingUsage.set(receipt.submissionId, slot);
|
|
@@ -70,32 +70,61 @@
|
|
|
70
70
|
* documented gap in both guarantees, not a bug this module can paper over
|
|
71
71
|
* from the outside.
|
|
72
72
|
*
|
|
73
|
-
* OUTBOUND OTEL PROPAGATION [OFFICIAL config surface;
|
|
74
|
-
* behavior]:
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
73
|
+
* OUTBOUND OTEL + GATEWAY-HEADER PROPAGATION [OFFICIAL config surface;
|
|
74
|
+
* best-effort behavior]: `request.otel` is present only when
|
|
75
|
+
* `observability.otel` is configured (`agents.ts`'s `send()`); `request.
|
|
76
|
+
* adw_id`/`request.agent_name` are present on EVERY call regardless (the
|
|
77
|
+
* gateway needs `x-correlation-id`/`x-spf-agent` whether or not SPF's own
|
|
78
|
+
* otel export is on — see `data_types.ts`'s `AgentRequest.adw_id` doc). This
|
|
79
|
+
* module propagates whatever subset of the two is present, two ways:
|
|
78
80
|
*
|
|
79
81
|
* 1. `TRACEPARENT` on the child's env — the standard W3C env var, same
|
|
80
|
-
* shape `agent_cc.ts` sets. No published
|
|
81
|
-
* opencode CLI itself reads it (UNVERIFIED
|
|
82
|
-
* and for any opencode-spawned subprocess
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
82
|
+
* shape `agent_cc.ts` sets, ONLY when `otel` is present. No published
|
|
83
|
+
* statement confirms the opencode CLI itself reads it (UNVERIFIED
|
|
84
|
+
* either way; set for parity and for any opencode-spawned subprocess
|
|
85
|
+
* telemetry, at zero cost).
|
|
86
|
+
* 2. `traceparent` (when `otel` present) and `x-correlation-id`/
|
|
87
|
+
* `x-spf-agent` (when `adw_id`/`agent_name` present) as STATIC provider
|
|
88
|
+
* headers in the temp `opencode.json` (`provider.<id>.options.headers`
|
|
89
|
+
* — opencode's documented per-provider options surface;
|
|
86
90
|
* https://opencode.ai/docs/providers/). Static values are CORRECT
|
|
87
91
|
* here, unlike the general case, because one `opencode run` subprocess
|
|
88
|
-
* IS exactly one SPF agent call —
|
|
89
|
-
* mid-run. This is the header that actually reaches the
|
|
90
|
-
* opencode's provider requests flow through the AI SDK, which
|
|
91
|
-
* `options.headers` for that provider's requests. Subject to
|
|
92
|
-
* CONFIG PRECEDENCE limitation above: a repo's own `opencode.json`
|
|
93
|
-
* override these headers.
|
|
92
|
+
* IS exactly one SPF agent call — none of these three headers can go
|
|
93
|
+
* stale mid-run. This is the header set that actually reaches the
|
|
94
|
+
* wire: opencode's provider requests flow through the AI SDK, which
|
|
95
|
+
* honors `options.headers` for that provider's requests. Subject to
|
|
96
|
+
* the CONFIG PRECEDENCE limitation above: a repo's own `opencode.json`
|
|
97
|
+
* can override these headers. `x-request-id` is NEVER one of them —
|
|
98
|
+
* Envoy/Switchyard own that header end-to-end; this module used to
|
|
99
|
+
* send it here (BLOCKER B) and no longer does.
|
|
94
100
|
*
|
|
95
101
|
* `injectOtelEnv()` / `otelProviderHeaders()` / `tempConfigContents()` are
|
|
96
102
|
* exported pure functions so every fragment is unit-testable without
|
|
97
103
|
* spawning a real subprocess — same discipline as the rest of this module.
|
|
98
104
|
*
|
|
105
|
+
* MINOR-4 (deliberate, not an oversight): `otelProviderHeaders()` returns
|
|
106
|
+
* non-null — and so `tempConfigContents()` writes a real `provider` block —
|
|
107
|
+
* whenever EITHER `otel` OR `gateway.adwId`/`gateway.agentName` is present,
|
|
108
|
+
* and `data_types.ts`'s `AgentRequest.adw_id`/`agent_name` doc is explicit
|
|
109
|
+
* that those two are set by `agents.ts`'s `send()` from `run.adw_id`/
|
|
110
|
+
* `agent.name` UNCONDITIONALLY — every SPF run has an adw_id, every agent
|
|
111
|
+
* has a name — NOT gated on `observability.otel` being configured at all.
|
|
112
|
+
* The practical consequence: for any `opencode` agent whose `--model`
|
|
113
|
+
* carries a `provider/` prefix (opencode's own documented model-id shape;
|
|
114
|
+
* see `otelProviderHeaders`'s own doc for the bare-model-name exception),
|
|
115
|
+
* this module writes a temporary `opencode.json` (`mkdtempSync` + one
|
|
116
|
+
* `writeFileSync`, removed in `run()`'s `finally`) on EVERY invocation, even
|
|
117
|
+
* with `observability.otel` unconfigured and no `tools:` restriction in
|
|
118
|
+
* play — there is no "otel off, no gateway identity, no tools restriction"
|
|
119
|
+
* case left in practice once a run has an adw_id, which is always. This is
|
|
120
|
+
* KEPT, not gated behind an extra "only when otel/tools apply" check: the
|
|
121
|
+
* gateway needs `x-correlation-id`/`x-spf-agent` on every real call to group
|
|
122
|
+
* it by run (same rationale as `ollama_provider.ts`'s static per-model
|
|
123
|
+
* headers), independent of whether tracing is turned on, and a temp file
|
|
124
|
+
* per subprocess invocation is cheap relative to spawning that subprocess
|
|
125
|
+
* at all. A caller that truly wants zero config-file overhead has no lever
|
|
126
|
+
* for that today short of clearing `adw_id`/`agent_name` on the request.
|
|
127
|
+
*
|
|
99
128
|
* OPERATOR-CONFIG MERGE: when a caller-provided `OPENCODE_CONFIG` already
|
|
100
129
|
* exists in the base env (operatorEnv() passthrough or an agent's
|
|
101
130
|
* env_allowlist) AND this module needs a temp config of its own (a tools:
|
|
@@ -257,22 +286,30 @@ export declare function isKnownToolName(name: string): boolean;
|
|
|
257
286
|
export declare function injectOtelEnv(baseEnv: Record<string, string>, otel: AgentRequest["otel"] | undefined): Record<string, string>;
|
|
258
287
|
export interface OpencodeOtelHeaders {
|
|
259
288
|
provider: string;
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
289
|
+
/** Never carries `x-request-id` — see the module doc comment's OUTBOUND OTEL + GATEWAY-HEADER PROPAGATION section. */
|
|
290
|
+
headers: Record<string, string>;
|
|
291
|
+
}
|
|
292
|
+
/** This call's own SPF identity, when the caller has it — see `data_types.ts`'s `AgentRequest.adw_id`/`agent_name` doc. Both optional; an absent one simply omits its header. */
|
|
293
|
+
export interface GatewayCallIdentity {
|
|
294
|
+
adwId?: string;
|
|
295
|
+
agentName?: string;
|
|
264
296
|
}
|
|
265
297
|
/**
|
|
266
298
|
* Builds the `provider.<id>.options.headers` fragment for a temp
|
|
267
|
-
* `opencode.json` (see the module doc comment's OUTBOUND OTEL
|
|
268
|
-
* section for why static values are correct here).
|
|
269
|
-
* `
|
|
299
|
+
* `opencode.json` (see the module doc comment's OUTBOUND OTEL + GATEWAY-
|
|
300
|
+
* HEADER PROPAGATION section for why static values are correct here).
|
|
301
|
+
* Returns `null` when NEITHER `otel` nor `gateway` has anything to
|
|
302
|
+
* contribute, or when the model id carries no `provider/` prefix —
|
|
270
303
|
* opencode's own `--model` vocabulary is documented as `provider/model-id`,
|
|
271
304
|
* but a bare model name has no provider id this module could key headers
|
|
272
305
|
* under, and guessing the wrong provider id would write a config block
|
|
273
306
|
* opencode merges onto a DIFFERENT provider than the one being called.
|
|
307
|
+
* `x-correlation-id`/`x-spf-agent` are added whenever `gateway` supplies
|
|
308
|
+
* them, regardless of whether `otel` is configured — see
|
|
309
|
+
* `data_types.ts`'s `AgentRequest.adw_id` doc for why that pair isn't
|
|
310
|
+
* gated on `observability.otel` the way `traceparent` is.
|
|
274
311
|
*/
|
|
275
|
-
export declare function otelProviderHeaders(model: string, otel: AgentRequest["otel"] | undefined): OpencodeOtelHeaders | null;
|
|
312
|
+
export declare function otelProviderHeaders(model: string, otel: AgentRequest["otel"] | undefined, gateway?: GatewayCallIdentity): OpencodeOtelHeaders | null;
|
|
276
313
|
/**
|
|
277
314
|
* Builds the JSON CONTENT of the temp `opencode.json`: a `permission` map
|
|
278
315
|
* when `toolNames` is an array (including `[]` — restrict to exactly those,
|
|
@@ -70,32 +70,61 @@
|
|
|
70
70
|
* documented gap in both guarantees, not a bug this module can paper over
|
|
71
71
|
* from the outside.
|
|
72
72
|
*
|
|
73
|
-
* OUTBOUND OTEL PROPAGATION [OFFICIAL config surface;
|
|
74
|
-
* behavior]:
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
73
|
+
* OUTBOUND OTEL + GATEWAY-HEADER PROPAGATION [OFFICIAL config surface;
|
|
74
|
+
* best-effort behavior]: `request.otel` is present only when
|
|
75
|
+
* `observability.otel` is configured (`agents.ts`'s `send()`); `request.
|
|
76
|
+
* adw_id`/`request.agent_name` are present on EVERY call regardless (the
|
|
77
|
+
* gateway needs `x-correlation-id`/`x-spf-agent` whether or not SPF's own
|
|
78
|
+
* otel export is on — see `data_types.ts`'s `AgentRequest.adw_id` doc). This
|
|
79
|
+
* module propagates whatever subset of the two is present, two ways:
|
|
78
80
|
*
|
|
79
81
|
* 1. `TRACEPARENT` on the child's env — the standard W3C env var, same
|
|
80
|
-
* shape `agent_cc.ts` sets. No published
|
|
81
|
-
* opencode CLI itself reads it (UNVERIFIED
|
|
82
|
-
* and for any opencode-spawned subprocess
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
82
|
+
* shape `agent_cc.ts` sets, ONLY when `otel` is present. No published
|
|
83
|
+
* statement confirms the opencode CLI itself reads it (UNVERIFIED
|
|
84
|
+
* either way; set for parity and for any opencode-spawned subprocess
|
|
85
|
+
* telemetry, at zero cost).
|
|
86
|
+
* 2. `traceparent` (when `otel` present) and `x-correlation-id`/
|
|
87
|
+
* `x-spf-agent` (when `adw_id`/`agent_name` present) as STATIC provider
|
|
88
|
+
* headers in the temp `opencode.json` (`provider.<id>.options.headers`
|
|
89
|
+
* — opencode's documented per-provider options surface;
|
|
86
90
|
* https://opencode.ai/docs/providers/). Static values are CORRECT
|
|
87
91
|
* here, unlike the general case, because one `opencode run` subprocess
|
|
88
|
-
* IS exactly one SPF agent call —
|
|
89
|
-
* mid-run. This is the header that actually reaches the
|
|
90
|
-
* opencode's provider requests flow through the AI SDK, which
|
|
91
|
-
* `options.headers` for that provider's requests. Subject to
|
|
92
|
-
* CONFIG PRECEDENCE limitation above: a repo's own `opencode.json`
|
|
93
|
-
* override these headers.
|
|
92
|
+
* IS exactly one SPF agent call — none of these three headers can go
|
|
93
|
+
* stale mid-run. This is the header set that actually reaches the
|
|
94
|
+
* wire: opencode's provider requests flow through the AI SDK, which
|
|
95
|
+
* honors `options.headers` for that provider's requests. Subject to
|
|
96
|
+
* the CONFIG PRECEDENCE limitation above: a repo's own `opencode.json`
|
|
97
|
+
* can override these headers. `x-request-id` is NEVER one of them —
|
|
98
|
+
* Envoy/Switchyard own that header end-to-end; this module used to
|
|
99
|
+
* send it here (BLOCKER B) and no longer does.
|
|
94
100
|
*
|
|
95
101
|
* `injectOtelEnv()` / `otelProviderHeaders()` / `tempConfigContents()` are
|
|
96
102
|
* exported pure functions so every fragment is unit-testable without
|
|
97
103
|
* spawning a real subprocess — same discipline as the rest of this module.
|
|
98
104
|
*
|
|
105
|
+
* MINOR-4 (deliberate, not an oversight): `otelProviderHeaders()` returns
|
|
106
|
+
* non-null — and so `tempConfigContents()` writes a real `provider` block —
|
|
107
|
+
* whenever EITHER `otel` OR `gateway.adwId`/`gateway.agentName` is present,
|
|
108
|
+
* and `data_types.ts`'s `AgentRequest.adw_id`/`agent_name` doc is explicit
|
|
109
|
+
* that those two are set by `agents.ts`'s `send()` from `run.adw_id`/
|
|
110
|
+
* `agent.name` UNCONDITIONALLY — every SPF run has an adw_id, every agent
|
|
111
|
+
* has a name — NOT gated on `observability.otel` being configured at all.
|
|
112
|
+
* The practical consequence: for any `opencode` agent whose `--model`
|
|
113
|
+
* carries a `provider/` prefix (opencode's own documented model-id shape;
|
|
114
|
+
* see `otelProviderHeaders`'s own doc for the bare-model-name exception),
|
|
115
|
+
* this module writes a temporary `opencode.json` (`mkdtempSync` + one
|
|
116
|
+
* `writeFileSync`, removed in `run()`'s `finally`) on EVERY invocation, even
|
|
117
|
+
* with `observability.otel` unconfigured and no `tools:` restriction in
|
|
118
|
+
* play — there is no "otel off, no gateway identity, no tools restriction"
|
|
119
|
+
* case left in practice once a run has an adw_id, which is always. This is
|
|
120
|
+
* KEPT, not gated behind an extra "only when otel/tools apply" check: the
|
|
121
|
+
* gateway needs `x-correlation-id`/`x-spf-agent` on every real call to group
|
|
122
|
+
* it by run (same rationale as `ollama_provider.ts`'s static per-model
|
|
123
|
+
* headers), independent of whether tracing is turned on, and a temp file
|
|
124
|
+
* per subprocess invocation is cheap relative to spawning that subprocess
|
|
125
|
+
* at all. A caller that truly wants zero config-file overhead has no lever
|
|
126
|
+
* for that today short of clearing `adw_id`/`agent_name` on the request.
|
|
127
|
+
*
|
|
99
128
|
* OPERATOR-CONFIG MERGE: when a caller-provided `OPENCODE_CONFIG` already
|
|
100
129
|
* exists in the base env (operatorEnv() passthrough or an agent's
|
|
101
130
|
* env_allowlist) AND this module needs a temp config of its own (a tools:
|
|
@@ -413,21 +442,33 @@ export function injectOtelEnv(baseEnv, otel) {
|
|
|
413
442
|
}
|
|
414
443
|
/**
|
|
415
444
|
* Builds the `provider.<id>.options.headers` fragment for a temp
|
|
416
|
-
* `opencode.json` (see the module doc comment's OUTBOUND OTEL
|
|
417
|
-
* section for why static values are correct here).
|
|
418
|
-
* `
|
|
445
|
+
* `opencode.json` (see the module doc comment's OUTBOUND OTEL + GATEWAY-
|
|
446
|
+
* HEADER PROPAGATION section for why static values are correct here).
|
|
447
|
+
* Returns `null` when NEITHER `otel` nor `gateway` has anything to
|
|
448
|
+
* contribute, or when the model id carries no `provider/` prefix —
|
|
419
449
|
* opencode's own `--model` vocabulary is documented as `provider/model-id`,
|
|
420
450
|
* but a bare model name has no provider id this module could key headers
|
|
421
451
|
* under, and guessing the wrong provider id would write a config block
|
|
422
452
|
* opencode merges onto a DIFFERENT provider than the one being called.
|
|
453
|
+
* `x-correlation-id`/`x-spf-agent` are added whenever `gateway` supplies
|
|
454
|
+
* them, regardless of whether `otel` is configured — see
|
|
455
|
+
* `data_types.ts`'s `AgentRequest.adw_id` doc for why that pair isn't
|
|
456
|
+
* gated on `observability.otel` the way `traceparent` is.
|
|
423
457
|
*/
|
|
424
|
-
export function otelProviderHeaders(model, otel) {
|
|
425
|
-
if (!otel)
|
|
426
|
-
return null;
|
|
458
|
+
export function otelProviderHeaders(model, otel, gateway = {}) {
|
|
427
459
|
const slash = model.indexOf("/");
|
|
428
460
|
if (slash <= 0)
|
|
429
461
|
return null;
|
|
430
|
-
|
|
462
|
+
const headers = {};
|
|
463
|
+
if (otel)
|
|
464
|
+
headers.traceparent = otel.traceparent;
|
|
465
|
+
if (gateway.adwId)
|
|
466
|
+
headers["x-correlation-id"] = gateway.adwId;
|
|
467
|
+
if (gateway.agentName)
|
|
468
|
+
headers["x-spf-agent"] = gateway.agentName;
|
|
469
|
+
if (Object.keys(headers).length === 0)
|
|
470
|
+
return null;
|
|
471
|
+
return { provider: model.slice(0, slash), headers };
|
|
431
472
|
}
|
|
432
473
|
/**
|
|
433
474
|
* Builds the JSON CONTENT of the temp `opencode.json`: a `permission` map
|
|
@@ -601,12 +642,12 @@ export async function run(request, onEvent, onSpawn, onExit) {
|
|
|
601
642
|
const [cmd, ...cmdArgs] = cmdTokens;
|
|
602
643
|
const fullArgs = [...cmdArgs, ...args];
|
|
603
644
|
const baseEnv = request.env ?? operatorEnv();
|
|
604
|
-
// `request.tools` null/undefined AND no otel config
|
|
605
|
-
// config file written at all (see tempConfigContents'
|
|
606
|
-
// Otherwise -> a real temp opencode.json (tool
|
|
607
|
-
// headers, or both), pointed at via
|
|
608
|
-
// only — never mutates process.env.
|
|
609
|
-
const otelHeaders = otelProviderHeaders(request.model, request.otel);
|
|
645
|
+
// `request.tools` null/undefined AND no otel config AND no adw_id/agent_name
|
|
646
|
+
// -> every tool, no config file written at all (see tempConfigContents'
|
|
647
|
+
// own doc comment). Otherwise -> a real temp opencode.json (tool
|
|
648
|
+
// restriction, gateway provider headers, or both), pointed at via
|
|
649
|
+
// OPENCODE_CONFIG on the CHILD's env only — never mutates process.env.
|
|
650
|
+
const otelHeaders = otelProviderHeaders(request.model, request.otel, { adwId: request.adw_id, agentName: request.agent_name });
|
|
610
651
|
let configContents = tempConfigContents(request.tools, otelHeaders);
|
|
611
652
|
// A caller-provided OPENCODE_CONFIG (operatorEnv() passthrough or an
|
|
612
653
|
// agent's env_allowlist) is MERGED into the temp file, never replaced —
|
package/dist/core/agents.d.ts
CHANGED
|
@@ -37,15 +37,56 @@ export declare class BudgetExceeded extends Error {
|
|
|
37
37
|
* message that says `$0.00 of max_run_cost $0.40` tells nobody anything.
|
|
38
38
|
*/
|
|
39
39
|
export declare function formatUsd(value: number): string;
|
|
40
|
+
/**
|
|
41
|
+
* True when THIS ONE AGENT, if dispatched right now, would report a
|
|
42
|
+
* gateway-estimated cost — a `claude_code` agent AND a non-Anthropic
|
|
43
|
+
* `ANTHROPIC_BASE_URL`. The per-agent primitive `Run.recordDispatch`
|
|
44
|
+
* (`runner.ts`) calls AT DISPATCH TIME, in `execute()` below, right before
|
|
45
|
+
* the real coding-agent call — so `run.cost_is_estimate` reflects what this
|
|
46
|
+
* run actually DISPATCHED, never what the roster merely makes possible (see
|
|
47
|
+
* `isGatewayEstimatedCost`'s own doc comment for why that distinction is
|
|
48
|
+
* the whole point of this function existing separately).
|
|
49
|
+
*
|
|
50
|
+
* `env` defaults to `process.env` (already carrying `cfg.env`'s own
|
|
51
|
+
* defaults — see `applyConfigEnv`, applied once at CLI startup before any
|
|
52
|
+
* `Run` is constructed) but is overridable so a test never touches the
|
|
53
|
+
* real environment.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isGatewayEstimatedDispatch(agent: AgentConfig, env?: Record<string, string | undefined>): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Whole-ROSTER check: true when ANY configured agent (dispatched or not)
|
|
58
|
+
* is a `claude_code` agent AND `ANTHROPIC_BASE_URL` is non-Anthropic.
|
|
59
|
+
*
|
|
60
|
+
* NOT what `Run.cost_is_estimate` is computed from — a chain can configure
|
|
61
|
+
* a `claude_code` agent it never actually dispatches this run (a
|
|
62
|
+
* conditional phase, a different `--agent` override, ...), and labeling a
|
|
63
|
+
* real, non-gateway cost as "estimated" because the ROSTER merely contains
|
|
64
|
+
* such an agent would be its own kind of dishonesty. `Run` instead starts
|
|
65
|
+
* `cost_is_estimate` at `false` and `recordDispatch()` (`runner.ts`, driven
|
|
66
|
+
* by `isGatewayEstimatedDispatch` above) flips it true only when a
|
|
67
|
+
* qualifying dispatch actually happens. This whole-roster version is kept
|
|
68
|
+
* as the general "could this config ever need the estimate label" check
|
|
69
|
+
* (`spf estimate`-shaped questions, and this file's own test suite) — never
|
|
70
|
+
* wire it back into the per-run label.
|
|
71
|
+
*/
|
|
72
|
+
export declare function isGatewayEstimatedCost(cfg: SFConfig, env?: Record<string, string | undefined>): boolean;
|
|
40
73
|
/**
|
|
41
74
|
* The accumulating totals a budget check reads. Structurally a subset of
|
|
42
|
-
* `Run` (`core/runner.ts`) — `run.tokens`/`run.cost`
|
|
43
|
-
* `run.addUsage()` after every send — so the real `Run`
|
|
44
|
-
* adapter, and a test can pass a plain object with fake
|
|
75
|
+
* `Run` (`core/runner.ts`) — `run.tokens`/`run.cost`/`run.billable_tokens`
|
|
76
|
+
* are incremented by `run.addUsage()` after every send — so the real `Run`
|
|
77
|
+
* satisfies it with no adapter, and a test can pass a plain object with fake
|
|
78
|
+
* usage.
|
|
79
|
+
*
|
|
80
|
+
* `tokens` (the display total, cache reads included) is kept on this
|
|
81
|
+
* interface for structural parity with `Run` even though `assertRunBudget`
|
|
82
|
+
* itself no longer reads it — only `billable_tokens` does. See
|
|
83
|
+
* `UsageBreakdown.billable_tokens`'s doc comment (`data_types.ts`) for why
|
|
84
|
+
* the two diverge.
|
|
45
85
|
*/
|
|
46
86
|
export interface RunBudgetState {
|
|
47
87
|
cfg: SFConfig;
|
|
48
88
|
tokens: number;
|
|
89
|
+
billable_tokens: number;
|
|
49
90
|
cost: number;
|
|
50
91
|
}
|
|
51
92
|
/**
|
|
@@ -122,6 +163,8 @@ interface RunForAgents {
|
|
|
122
163
|
/** Run-total tokens/cost so far, mirrored by `addUsage` below — read by `assertRunBudget` before every send. */
|
|
123
164
|
tokens: number;
|
|
124
165
|
cost: number;
|
|
166
|
+
/** The BILLABLE half of `tokens` — what `assertRunBudget` actually checks `max_run_tokens` against. See `UsageBreakdown.billable_tokens`'s doc comment. */
|
|
167
|
+
billable_tokens: number;
|
|
125
168
|
repo_root: string;
|
|
126
169
|
spf_dir: string | null;
|
|
127
170
|
data_dir: string;
|
|
@@ -159,8 +202,12 @@ interface RunForAgents {
|
|
|
159
202
|
retry: (name: string, attempt: number, limit: number, reason: string) => Promise<void>;
|
|
160
203
|
envelopeSummary: (envelope: EnvelopeBase, typeName: string) => Promise<void>;
|
|
161
204
|
agentFinished: (name: string, tokens: number, cost: number) => Promise<void>;
|
|
205
|
+
/** Free-form detail line — used here only to name a `defaults.read_only_ignore` restore so it's visible without failing the phase. */
|
|
206
|
+
note: (message: string) => Promise<void>;
|
|
162
207
|
};
|
|
163
|
-
addUsage: (tokens: number, cost: number) => Promise<void>;
|
|
208
|
+
addUsage: (tokens: number, cost: number, billableTokens: number) => Promise<void>;
|
|
209
|
+
/** See `Run.recordDispatch`'s own doc comment (`runner.ts`) and `isGatewayEstimatedDispatch` above — called once per real dispatch, before it happens. */
|
|
210
|
+
recordDispatch: (agent: AgentConfig) => void;
|
|
164
211
|
saveAgentMap: (agent: string, entry: {
|
|
165
212
|
session_id: string;
|
|
166
213
|
model: string;
|
package/dist/core/agents.js
CHANGED
|
@@ -69,6 +69,68 @@ export function formatUsd(value) {
|
|
|
69
69
|
const fixed = value.toFixed(3);
|
|
70
70
|
return `$${fixed.endsWith("0") ? fixed.slice(0, -1) : fixed}`;
|
|
71
71
|
}
|
|
72
|
+
/** Anthropic's own API — a `claude_code` agent talking to exactly this is billed honestly; anything else is a gateway/proxy standing in for it. */
|
|
73
|
+
const ANTHROPIC_DEFAULT_BASE_URL_RE = /^https:\/\/api\.anthropic\.com\/?$/i;
|
|
74
|
+
/**
|
|
75
|
+
* WHY THIS MATTERS: `agent_cc.ts`'s `run()` reports `final.total_cost_usd`
|
|
76
|
+
* (the `claude` CLI's OWN number) into `UsageBreakdown.total_cost` verbatim
|
|
77
|
+
* — that figure is Anthropic's price table applied to whichever model the
|
|
78
|
+
* CLI thinks it called, computed CLIENT-SIDE with no visibility into what
|
|
79
|
+
* actually served the request. Pointed at a gateway (Ollama Cloud, a
|
|
80
|
+
* Cloudflare AI Gateway proxy, ...) fronting a different provider/billing
|
|
81
|
+
* model entirely (subscription, flat per-token gateway price, ...), that
|
|
82
|
+
* number is a plausible-looking ESTIMATE of what Anthropic would have
|
|
83
|
+
* charged for this many tokens — not a fact about what was actually billed.
|
|
84
|
+
* `Console.sessionFinished` reads this to change the run summary's "cost"
|
|
85
|
+
* line from a bare dollar figure to a labeled estimate rather than silently
|
|
86
|
+
* presenting a guess as ground truth.
|
|
87
|
+
*/
|
|
88
|
+
function usesNonAnthropicGateway(env) {
|
|
89
|
+
const baseUrl = env["ANTHROPIC_BASE_URL"];
|
|
90
|
+
if (!baseUrl || !baseUrl.trim())
|
|
91
|
+
return false;
|
|
92
|
+
return !ANTHROPIC_DEFAULT_BASE_URL_RE.test(baseUrl.trim());
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* True when THIS ONE AGENT, if dispatched right now, would report a
|
|
96
|
+
* gateway-estimated cost — a `claude_code` agent AND a non-Anthropic
|
|
97
|
+
* `ANTHROPIC_BASE_URL`. The per-agent primitive `Run.recordDispatch`
|
|
98
|
+
* (`runner.ts`) calls AT DISPATCH TIME, in `execute()` below, right before
|
|
99
|
+
* the real coding-agent call — so `run.cost_is_estimate` reflects what this
|
|
100
|
+
* run actually DISPATCHED, never what the roster merely makes possible (see
|
|
101
|
+
* `isGatewayEstimatedCost`'s own doc comment for why that distinction is
|
|
102
|
+
* the whole point of this function existing separately).
|
|
103
|
+
*
|
|
104
|
+
* `env` defaults to `process.env` (already carrying `cfg.env`'s own
|
|
105
|
+
* defaults — see `applyConfigEnv`, applied once at CLI startup before any
|
|
106
|
+
* `Run` is constructed) but is overridable so a test never touches the
|
|
107
|
+
* real environment.
|
|
108
|
+
*/
|
|
109
|
+
export function isGatewayEstimatedDispatch(agent, env = process.env) {
|
|
110
|
+
return agent.coding_agent === "claude_code" && usesNonAnthropicGateway(env);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Whole-ROSTER check: true when ANY configured agent (dispatched or not)
|
|
114
|
+
* is a `claude_code` agent AND `ANTHROPIC_BASE_URL` is non-Anthropic.
|
|
115
|
+
*
|
|
116
|
+
* NOT what `Run.cost_is_estimate` is computed from — a chain can configure
|
|
117
|
+
* a `claude_code` agent it never actually dispatches this run (a
|
|
118
|
+
* conditional phase, a different `--agent` override, ...), and labeling a
|
|
119
|
+
* real, non-gateway cost as "estimated" because the ROSTER merely contains
|
|
120
|
+
* such an agent would be its own kind of dishonesty. `Run` instead starts
|
|
121
|
+
* `cost_is_estimate` at `false` and `recordDispatch()` (`runner.ts`, driven
|
|
122
|
+
* by `isGatewayEstimatedDispatch` above) flips it true only when a
|
|
123
|
+
* qualifying dispatch actually happens. This whole-roster version is kept
|
|
124
|
+
* as the general "could this config ever need the estimate label" check
|
|
125
|
+
* (`spf estimate`-shaped questions, and this file's own test suite) — never
|
|
126
|
+
* wire it back into the per-run label.
|
|
127
|
+
*/
|
|
128
|
+
export function isGatewayEstimatedCost(cfg, env = process.env) {
|
|
129
|
+
const usesClaudeCode = cfg.agents.some((a) => a.coding_agent === "claude_code");
|
|
130
|
+
if (!usesClaudeCode)
|
|
131
|
+
return false;
|
|
132
|
+
return usesNonAnthropicGateway(env);
|
|
133
|
+
}
|
|
72
134
|
/**
|
|
73
135
|
* Throw if this run has spent its budget. Called BEFORE every agent
|
|
74
136
|
* dispatch, never after.
|
|
@@ -92,8 +154,8 @@ export function assertRunBudget(run) {
|
|
|
92
154
|
throw new BudgetExceeded(`run budget exceeded: ${formatUsd(run.cost)} of max_run_cost ${formatUsd(maxCost)} — ` +
|
|
93
155
|
`raise defaults.max_run_cost or split the work`);
|
|
94
156
|
}
|
|
95
|
-
if (maxTokens !== undefined && run.
|
|
96
|
-
throw new BudgetExceeded(`run budget exceeded: ${run.
|
|
157
|
+
if (maxTokens !== undefined && run.billable_tokens >= maxTokens) {
|
|
158
|
+
throw new BudgetExceeded(`run budget exceeded: ${run.billable_tokens.toLocaleString("en-US")} tokens of max_run_tokens ` +
|
|
97
159
|
`${maxTokens.toLocaleString("en-US")} — raise defaults.max_run_tokens or split the work`);
|
|
98
160
|
}
|
|
99
161
|
}
|
|
@@ -833,8 +895,11 @@ export async function execute(run, phase, call) {
|
|
|
833
895
|
output_type_name: call.output_type.name,
|
|
834
896
|
cwd: spec ? spec.workspace_dir : run.repo_root,
|
|
835
897
|
flue_db_path: path.join(run.data_dir, "flue.db"),
|
|
898
|
+
request_timeout_ms: run.cfg.defaults.request_timeout_ms,
|
|
836
899
|
env: agentEnv(agent),
|
|
837
900
|
sandbox: spec,
|
|
901
|
+
adw_id: run.adw_id,
|
|
902
|
+
agent_name: agent.name,
|
|
838
903
|
otel: otelCtx && otelBlock
|
|
839
904
|
? {
|
|
840
905
|
traceparent: otelCtx.traceparent,
|
|
@@ -854,6 +919,9 @@ export async function execute(run, phase, call) {
|
|
|
854
919
|
const onExit = (pid) => void run.tracer.processEnd(run.adw_id, pid).catch(() => { });
|
|
855
920
|
if (spec)
|
|
856
921
|
await sandbox.reconcileWorkspace(spec);
|
|
922
|
+
// Recorded right before the real dispatch, not derived from the roster
|
|
923
|
+
// up front — see `isGatewayEstimatedDispatch`'s doc comment for why.
|
|
924
|
+
run.recordDispatch(agent);
|
|
857
925
|
let result;
|
|
858
926
|
if (agent.coding_agent === "claude_code") {
|
|
859
927
|
result = await agentCc.run(request, forward, onSpawn, onExit);
|
|
@@ -866,7 +934,7 @@ export async function execute(run, phase, call) {
|
|
|
866
934
|
}
|
|
867
935
|
// SPEND IS RECORDED BEFORE THE EXTRACT CAN THROW: a failed extract must
|
|
868
936
|
// not also lose this call's tokens/cost off the Run's ledger.
|
|
869
|
-
await run.addUsage(result.tokens, result.cost);
|
|
937
|
+
await run.addUsage(result.tokens, result.cost, result.usage.billable_tokens);
|
|
870
938
|
spent.merge(result.usage);
|
|
871
939
|
// opencode-ONLY: every subsequent send() in THIS phase (JSON-repair
|
|
872
940
|
// retries, gate corrections) must target the real captured session, not
|
|
@@ -934,7 +1002,14 @@ export async function execute(run, phase, call) {
|
|
|
934
1002
|
// wrote somewhere it was not allowed to.
|
|
935
1003
|
let touched;
|
|
936
1004
|
try {
|
|
937
|
-
touched = permissions.enforce(run, phase, agent, treeBefore)
|
|
1005
|
+
touched = permissions.enforce(run, phase, agent, treeBefore, (ignoredPaths) => {
|
|
1006
|
+
// Fire-and-forget, same discipline as onSpawn/onExit above: this is a
|
|
1007
|
+
// logging side effect off the main control flow, not something a
|
|
1008
|
+
// failed write here should turn into an unhandled rejection.
|
|
1009
|
+
void run.console
|
|
1010
|
+
.note(`${agent.name}: restored and ignored dependency-lockfile churn (defaults.read_only_ignore) without failing the phase: ${ignoredPaths.join(", ")}`)
|
|
1011
|
+
.catch(() => { });
|
|
1012
|
+
});
|
|
938
1013
|
}
|
|
939
1014
|
catch (breach) {
|
|
940
1015
|
await run.tracer.event(makeEventRecord({
|
package/dist/core/console.d.ts
CHANGED
|
@@ -27,7 +27,15 @@ interface Tracer {
|
|
|
27
27
|
export interface RunObserver {
|
|
28
28
|
onPhaseStart?(phase: Phase): void;
|
|
29
29
|
onPhaseEnd?(phase: Phase, seconds: number): void;
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* `tokens` is the DISPLAY total (every re-sent token, cache reads
|
|
32
|
+
* included — context occupancy); `billableTokens` is what
|
|
33
|
+
* `defaults.max_run_tokens` actually checks (see
|
|
34
|
+
* `UsageBreakdown.billable_tokens`'s doc comment in `data_types.ts`). A
|
|
35
|
+
* dashboard comparing spend against the ceiling must compare
|
|
36
|
+
* `billableTokens`, never `tokens` — see `run_dashboard.tsx`.
|
|
37
|
+
*/
|
|
38
|
+
onUsage?(tokens: number, cost: number, billableTokens: number): void;
|
|
31
39
|
onSessionEnd?(ok: boolean): void;
|
|
32
40
|
}
|
|
33
41
|
/** Bound to one run's tracer. Reachable as `run.console` everywhere. */
|
|
@@ -71,13 +79,25 @@ export declare class Console {
|
|
|
71
79
|
observer?: RunObserver | null);
|
|
72
80
|
private emit;
|
|
73
81
|
sessionStarted(adwId: string, engineer: string): Promise<void>;
|
|
74
|
-
|
|
82
|
+
/**
|
|
83
|
+
* `costIsEstimate` (default `false`, byte-identical to before this param
|
|
84
|
+
* existed): true when `run.cost_is_estimate` found a `claude_code` agent
|
|
85
|
+
* pointed at a non-Anthropic `ANTHROPIC_BASE_URL` (see `runner.ts`'s
|
|
86
|
+
* `Run.recordDispatch` / `agents.ts`'s `isGatewayEstimatedDispatch`) —
|
|
87
|
+
* `total_cost_usd` from `claude`'s own CLI is
|
|
88
|
+
* Anthropic's price table applied client-side, which is a fact only when
|
|
89
|
+
* Anthropic itself served the request, and a labeled guess otherwise. The
|
|
90
|
+
* label is cosmetic only: `cost` itself is unchanged (still the real sum
|
|
91
|
+
* `UsageBreakdown.total_cost` accumulated), and nothing about the budget
|
|
92
|
+
* check (`assertRunBudget`) reads this flag.
|
|
93
|
+
*/
|
|
94
|
+
sessionFinished(ok: boolean, tokens: number, cost: number, dbPath: string, costIsEstimate?: boolean): Promise<void>;
|
|
75
95
|
phaseStarted(phase: Phase): Promise<void>;
|
|
76
96
|
phaseEnded(phase: Phase, seconds: number): Promise<void>;
|
|
77
97
|
/** Free-form detail inside the current phase — what `ph.log()` recorded. */
|
|
78
98
|
note(message: string): Promise<void>;
|
|
79
|
-
/** `Run.addUsage()`'s only hook into `Console` — the running
|
|
80
|
-
notifyUsage(tokens: number, cost: number): Promise<void>;
|
|
99
|
+
/** `Run.addUsage()`'s only hook into `Console` — the running totals live on `Run`, not here, so this just forwards them to the observer. No line prints for this on its own; the totals already show up in `sessionFinished`'s panel. `billableTokens` rides alongside `tokens` so a consumer comparing against `defaults.max_run_tokens` (a billable ceiling) never has to guess which number to use — see `RunObserver.onUsage`'s own doc comment. */
|
|
100
|
+
notifyUsage(tokens: number, cost: number, billableTokens: number): Promise<void>;
|
|
81
101
|
agentStarted(name: string, model: string, sessionId: string): Promise<void>;
|
|
82
102
|
agentFinished(name: string, tokens: number, cost: number): Promise<void>;
|
|
83
103
|
retry(name: string, attempt: number, limit: number, reason: string): Promise<void>;
|