patchwork-os 0.2.0-beta.10.canary.99 → 0.2.0-beta.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.
Files changed (227) hide show
  1. package/README.md +20 -1
  2. package/dist/adapters/grok.js +6 -1
  3. package/dist/adapters/grok.js.map +1 -1
  4. package/dist/approvalHttp.js +61 -66
  5. package/dist/approvalHttp.js.map +1 -1
  6. package/dist/bridge.js +3 -2
  7. package/dist/bridge.js.map +1 -1
  8. package/dist/claudeOrchestrator.js +15 -0
  9. package/dist/claudeOrchestrator.js.map +1 -1
  10. package/dist/commands/patchworkInit.d.ts +7 -0
  11. package/dist/commands/patchworkInit.js +26 -0
  12. package/dist/commands/patchworkInit.js.map +1 -1
  13. package/dist/commands/recipe.d.ts +14 -0
  14. package/dist/commands/recipe.js +51 -0
  15. package/dist/commands/recipe.js.map +1 -1
  16. package/dist/commands/recipeInstall.js +15 -0
  17. package/dist/commands/recipeInstall.js.map +1 -1
  18. package/dist/connectors/postgres.d.ts +7 -0
  19. package/dist/connectors/postgres.js +16 -2
  20. package/dist/connectors/postgres.js.map +1 -1
  21. package/dist/connectors/snowflake.js +6 -0
  22. package/dist/connectors/snowflake.js.map +1 -1
  23. package/dist/connectors/tokenStorage.d.ts +8 -0
  24. package/dist/connectors/tokenStorage.js +42 -3
  25. package/dist/connectors/tokenStorage.js.map +1 -1
  26. package/dist/drivers/claude/api.js +15 -2
  27. package/dist/drivers/claude/api.js.map +1 -1
  28. package/dist/drivers/claude/subprocess.js +1 -29
  29. package/dist/drivers/claude/subprocess.js.map +1 -1
  30. package/dist/drivers/gemini/index.js +9 -5
  31. package/dist/drivers/gemini/index.js.map +1 -1
  32. package/dist/drivers/openai/index.js +29 -4
  33. package/dist/drivers/openai/index.js.map +1 -1
  34. package/dist/drivers/outputCap.d.ts +27 -0
  35. package/dist/drivers/outputCap.js +50 -0
  36. package/dist/drivers/outputCap.js.map +1 -0
  37. package/dist/featureFlags.d.ts +9 -0
  38. package/dist/featureFlags.js +16 -0
  39. package/dist/featureFlags.js.map +1 -1
  40. package/dist/fp/commandDescription.d.ts +6 -0
  41. package/dist/fp/commandDescription.js +4 -4
  42. package/dist/fp/commandDescription.js.map +1 -1
  43. package/dist/fp/policyParser.js +13 -7
  44. package/dist/fp/policyParser.js.map +1 -1
  45. package/dist/haltPushDispatch.js +7 -22
  46. package/dist/haltPushDispatch.js.map +1 -1
  47. package/dist/index.js +128 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/mcpRoutes.js +1 -1
  50. package/dist/mcpRoutes.js.map +1 -1
  51. package/dist/oauth.d.ts +13 -0
  52. package/dist/oauth.js +34 -1
  53. package/dist/oauth.js.map +1 -1
  54. package/dist/recipeOrchestration.js +10 -0
  55. package/dist/recipeOrchestration.js.map +1 -1
  56. package/dist/recipeRoutes.d.ts +39 -0
  57. package/dist/recipeRoutes.js +214 -12
  58. package/dist/recipeRoutes.js.map +1 -1
  59. package/dist/recipes/agentExecutor.d.ts +20 -0
  60. package/dist/recipes/agentExecutor.js +32 -10
  61. package/dist/recipes/agentExecutor.js.map +1 -1
  62. package/dist/recipes/chainedRunner.d.ts +41 -2
  63. package/dist/recipes/chainedRunner.js +134 -6
  64. package/dist/recipes/chainedRunner.js.map +1 -1
  65. package/dist/recipes/connectorPreflight.js +30 -0
  66. package/dist/recipes/connectorPreflight.js.map +1 -1
  67. package/dist/recipes/haltCategory.d.ts +5 -0
  68. package/dist/recipes/haltCategory.js +7 -1
  69. package/dist/recipes/haltCategory.js.map +1 -1
  70. package/dist/recipes/judgeVerdict.d.ts +10 -0
  71. package/dist/recipes/judgeVerdict.js +10 -0
  72. package/dist/recipes/judgeVerdict.js.map +1 -1
  73. package/dist/recipes/names.d.ts +5 -0
  74. package/dist/recipes/names.js +10 -5
  75. package/dist/recipes/names.js.map +1 -1
  76. package/dist/recipes/parser.js +32 -9
  77. package/dist/recipes/parser.js.map +1 -1
  78. package/dist/recipes/pricing/costRouter.d.ts +43 -0
  79. package/dist/recipes/pricing/costRouter.js +44 -0
  80. package/dist/recipes/pricing/costRouter.js.map +1 -0
  81. package/dist/recipes/pricing/priceTable.d.ts +76 -0
  82. package/dist/recipes/pricing/priceTable.js +144 -0
  83. package/dist/recipes/pricing/priceTable.js.map +1 -0
  84. package/dist/recipes/runBudget.d.ts +82 -32
  85. package/dist/recipes/runBudget.js +182 -49
  86. package/dist/recipes/runBudget.js.map +1 -1
  87. package/dist/recipes/schema.d.ts +25 -4
  88. package/dist/recipes/schemaGenerator.d.ts +9 -0
  89. package/dist/recipes/schemaGenerator.js +386 -75
  90. package/dist/recipes/schemaGenerator.js.map +1 -1
  91. package/dist/recipes/simulation/aggregateRunRisk.d.ts +43 -0
  92. package/dist/recipes/simulation/aggregateRunRisk.js +117 -0
  93. package/dist/recipes/simulation/aggregateRunRisk.js.map +1 -0
  94. package/dist/recipes/simulation/costProjector.d.ts +32 -0
  95. package/dist/recipes/simulation/costProjector.js +194 -0
  96. package/dist/recipes/simulation/costProjector.js.map +1 -0
  97. package/dist/recipes/simulation/sideEffects.d.ts +32 -0
  98. package/dist/recipes/simulation/sideEffects.js +62 -0
  99. package/dist/recipes/simulation/sideEffects.js.map +1 -0
  100. package/dist/recipes/simulation/simulate.d.ts +36 -0
  101. package/dist/recipes/simulation/simulate.js +264 -0
  102. package/dist/recipes/simulation/simulate.js.map +1 -0
  103. package/dist/recipes/simulation/simulateMockedRun.d.ts +52 -0
  104. package/dist/recipes/simulation/simulateMockedRun.js +72 -0
  105. package/dist/recipes/simulation/simulateMockedRun.js.map +1 -0
  106. package/dist/recipes/simulation/synthesizeMockedOutputs.d.ts +31 -0
  107. package/dist/recipes/simulation/synthesizeMockedOutputs.js +50 -0
  108. package/dist/recipes/simulation/synthesizeMockedOutputs.js.map +1 -0
  109. package/dist/recipes/simulation/types.d.ts +198 -0
  110. package/dist/recipes/simulation/types.js +30 -0
  111. package/dist/recipes/simulation/types.js.map +1 -0
  112. package/dist/recipes/stepObservation.d.ts +12 -0
  113. package/dist/recipes/stepObservation.js +21 -1
  114. package/dist/recipes/stepObservation.js.map +1 -1
  115. package/dist/recipes/tools/airtable.d.ts +15 -0
  116. package/dist/recipes/tools/airtable.js +240 -0
  117. package/dist/recipes/tools/airtable.js.map +1 -0
  118. package/dist/recipes/tools/caldiy.d.ts +13 -0
  119. package/dist/recipes/tools/caldiy.js +214 -0
  120. package/dist/recipes/tools/caldiy.js.map +1 -0
  121. package/dist/recipes/tools/circleci.d.ts +10 -0
  122. package/dist/recipes/tools/circleci.js +204 -0
  123. package/dist/recipes/tools/circleci.js.map +1 -0
  124. package/dist/recipes/tools/cloudflare.d.ts +13 -0
  125. package/dist/recipes/tools/cloudflare.js +211 -0
  126. package/dist/recipes/tools/cloudflare.js.map +1 -0
  127. package/dist/recipes/tools/docs.d.ts +18 -0
  128. package/dist/recipes/tools/docs.js +94 -0
  129. package/dist/recipes/tools/docs.js.map +1 -0
  130. package/dist/recipes/tools/elasticsearch.d.ts +11 -0
  131. package/dist/recipes/tools/elasticsearch.js +156 -0
  132. package/dist/recipes/tools/elasticsearch.js.map +1 -0
  133. package/dist/recipes/tools/figma.d.ts +12 -0
  134. package/dist/recipes/tools/figma.js +194 -0
  135. package/dist/recipes/tools/figma.js.map +1 -0
  136. package/dist/recipes/tools/grafana.d.ts +11 -0
  137. package/dist/recipes/tools/grafana.js +216 -0
  138. package/dist/recipes/tools/grafana.js.map +1 -0
  139. package/dist/recipes/tools/http.d.ts +1 -1
  140. package/dist/recipes/tools/http.js +10 -28
  141. package/dist/recipes/tools/http.js.map +1 -1
  142. package/dist/recipes/tools/index.d.ts +26 -0
  143. package/dist/recipes/tools/index.js +26 -0
  144. package/dist/recipes/tools/index.js.map +1 -1
  145. package/dist/recipes/tools/monday.d.ts +22 -0
  146. package/dist/recipes/tools/monday.js +242 -0
  147. package/dist/recipes/tools/monday.js.map +1 -0
  148. package/dist/recipes/tools/obsidian.d.ts +15 -0
  149. package/dist/recipes/tools/obsidian.js +172 -0
  150. package/dist/recipes/tools/obsidian.js.map +1 -0
  151. package/dist/recipes/tools/paystack.d.ts +11 -0
  152. package/dist/recipes/tools/paystack.js +211 -0
  153. package/dist/recipes/tools/paystack.js.map +1 -0
  154. package/dist/recipes/tools/pipedrive.d.ts +16 -0
  155. package/dist/recipes/tools/pipedrive.js +233 -0
  156. package/dist/recipes/tools/pipedrive.js.map +1 -0
  157. package/dist/recipes/tools/postgres.d.ts +15 -0
  158. package/dist/recipes/tools/postgres.js +185 -0
  159. package/dist/recipes/tools/postgres.js.map +1 -0
  160. package/dist/recipes/tools/posthog.d.ts +16 -0
  161. package/dist/recipes/tools/posthog.js +219 -0
  162. package/dist/recipes/tools/posthog.js.map +1 -0
  163. package/dist/recipes/tools/redis.d.ts +9 -0
  164. package/dist/recipes/tools/redis.js +140 -0
  165. package/dist/recipes/tools/redis.js.map +1 -0
  166. package/dist/recipes/tools/resend.d.ts +8 -0
  167. package/dist/recipes/tools/resend.js +153 -0
  168. package/dist/recipes/tools/resend.js.map +1 -0
  169. package/dist/recipes/tools/salesforce.d.ts +16 -0
  170. package/dist/recipes/tools/salesforce.js +184 -0
  171. package/dist/recipes/tools/salesforce.js.map +1 -0
  172. package/dist/recipes/tools/sendgrid.d.ts +9 -0
  173. package/dist/recipes/tools/sendgrid.js +174 -0
  174. package/dist/recipes/tools/sendgrid.js.map +1 -0
  175. package/dist/recipes/tools/shopify.d.ts +16 -0
  176. package/dist/recipes/tools/shopify.js +265 -0
  177. package/dist/recipes/tools/shopify.js.map +1 -0
  178. package/dist/recipes/tools/snowflake.d.ts +16 -0
  179. package/dist/recipes/tools/snowflake.js +173 -0
  180. package/dist/recipes/tools/snowflake.js.map +1 -0
  181. package/dist/recipes/tools/stripe.js +13 -12
  182. package/dist/recipes/tools/stripe.js.map +1 -1
  183. package/dist/recipes/tools/supabase.d.ts +9 -0
  184. package/dist/recipes/tools/supabase.js +132 -0
  185. package/dist/recipes/tools/supabase.js.map +1 -0
  186. package/dist/recipes/tools/todoist.d.ts +15 -0
  187. package/dist/recipes/tools/todoist.js +227 -0
  188. package/dist/recipes/tools/todoist.js.map +1 -0
  189. package/dist/recipes/tools/twilio.d.ts +11 -0
  190. package/dist/recipes/tools/twilio.js +180 -0
  191. package/dist/recipes/tools/twilio.js.map +1 -0
  192. package/dist/recipes/tools/vercel.d.ts +9 -0
  193. package/dist/recipes/tools/vercel.js +145 -0
  194. package/dist/recipes/tools/vercel.js.map +1 -0
  195. package/dist/recipes/tools/webflow.d.ts +11 -0
  196. package/dist/recipes/tools/webflow.js +243 -0
  197. package/dist/recipes/tools/webflow.js.map +1 -0
  198. package/dist/recipes/tools/woocommerce.d.ts +18 -0
  199. package/dist/recipes/tools/woocommerce.js +259 -0
  200. package/dist/recipes/tools/woocommerce.js.map +1 -0
  201. package/dist/recipes/tools/wrapConnectorExecute.d.ts +25 -0
  202. package/dist/recipes/tools/wrapConnectorExecute.js +37 -0
  203. package/dist/recipes/tools/wrapConnectorExecute.js.map +1 -0
  204. package/dist/recipes/validation.js +264 -0
  205. package/dist/recipes/validation.js.map +1 -1
  206. package/dist/recipes/yamlRunner.d.ts +112 -3
  207. package/dist/recipes/yamlRunner.js +482 -19
  208. package/dist/recipes/yamlRunner.js.map +1 -1
  209. package/dist/runLog.d.ts +46 -0
  210. package/dist/runLog.js +4 -0
  211. package/dist/runLog.js.map +1 -1
  212. package/dist/schemas/recipe.v1.json +217 -11
  213. package/dist/server.d.ts +3 -0
  214. package/dist/server.js +4 -0
  215. package/dist/server.js.map +1 -1
  216. package/dist/streamableHttp.js +3 -2
  217. package/dist/streamableHttp.js.map +1 -1
  218. package/dist/tools/gitWrite.js +5 -1
  219. package/dist/tools/gitWrite.js.map +1 -1
  220. package/dist/tools/terminal.js +15 -49
  221. package/dist/tools/terminal.js.map +1 -1
  222. package/dist/transport.d.ts +1 -0
  223. package/dist/transport.js +7 -0
  224. package/dist/transport.js.map +1 -1
  225. package/package.json +3 -3
  226. package/scripts/mcp-stdio-shim.cjs +218 -84
  227. package/templates/recipes/incident-to-pr.yaml +187 -0
@@ -1,30 +1,29 @@
1
1
  /**
2
- * Per-recipe token budget — PR2b.
2
+ * Per-recipe budget — PR2b (tokens) + cost-routing Phase 3 (USD).
3
3
  *
4
- * Built on PR2a's `AgentResult.usage` plumbing. The runner constructs
5
- * one `RunBudget` per recipe execution; agent steps consult it before
6
- * dispatch (admission) and reconcile actual consumption after the call
7
- * returns. On breach the run halts at the *next* admission check (we
8
- * never retroactively fail a step that succeeded — the user's tokens
9
- * are already spent, halting after-the-fact would just confuse the
10
- * audit trail).
4
+ * Built on PR2a's `AgentResult.usage` plumbing. The runner constructs one
5
+ * `RunBudget` per recipe execution; agent steps consult it before dispatch
6
+ * (admission) and reconcile actual consumption after the call returns. On
7
+ * breach the run halts at the *next* admission check (we never retroactively
8
+ * fail a step that succeeded — the user's tokens/dollars are already spent;
9
+ * halting after-the-fact would just confuse the audit trail).
11
10
  *
12
- * Subscription drivers (Claude CLI, provider subprocess CLIs) don't
13
- * surface per-call token counts. `RunBudget` tracks which drivers were
14
- * unmeasured and exposes a deduped warnings list fail-open with a
15
- * single notice per driver per run, never block.
11
+ * Two independent caps, either or both:
12
+ * - `tokensMax` cumulative input + output tokens.
13
+ * - `usdMax` — cumulative USD, priced from token usage via the Phase 2
14
+ * price table (`costUsd`).
16
15
  *
17
- * Scope (PR2b only):
18
- * - cumulative token enforcement (input + output)
19
- * - halt-on-breach OR warn-on-breach via `BudgetPolicy.onBreach`
20
- * - subscription-driver fail-open with warning
21
- *
22
- * Out of scope (future):
23
- * - per-step caps (extend `BudgetPolicy` or add `AgentStep.tokensMax`)
24
- * - `usdMax` (needs a price table; subscription drivers complicate)
25
- * - `wallClockMs` (orthogonal to tokens, separate accounting)
16
+ * Subscription drivers (Claude CLI, provider subprocess CLIs) don't surface
17
+ * per-call token counts, and a model with no price-table entry can't be
18
+ * costed. Both cases FAIL OPEN: `RunBudget` records a deduped one-time
19
+ * warning and never blocks. So a USD cap enforces for *measured + priced*
20
+ * (API) drivers and is a no-op-with-notice for everything else — never a
21
+ * silent or a surprise halt on the token-blind default driver. (The opt-in
22
+ * estimate-the-unmeasured path is a planned follow-up; absent it, unmeasured
23
+ * = fail-open, which is the conservative default the design calls for.)
26
24
  */
27
- import type { AgentUsage } from "./agentExecutor.js";
25
+ import { type AgentUsage } from "./agentExecutor.js";
26
+ import { type PriceTable } from "./pricing/priceTable.js";
28
27
  import type { BudgetPolicy } from "./schema.js";
29
28
  export interface BudgetAdmission {
30
29
  /** True = step may proceed. False = budget exhausted. */
@@ -36,35 +35,86 @@ export interface BudgetTotals {
36
35
  inputTokens: number;
37
36
  outputTokens: number;
38
37
  total: number;
39
- /** Remaining tokens before breach. Undefined when no limit is set. */
38
+ /** Remaining tokens before breach. Undefined when no token limit is set. */
40
39
  remaining?: number;
41
- /** True once total >= tokensMax. */
40
+ /** Cumulative measured USD. Undefined when no USD limit is set. */
41
+ usd?: number;
42
+ /** Remaining USD before breach. Undefined when no USD limit is set. */
43
+ usdRemaining?: number;
44
+ /**
45
+ * Notional list-price USD estimated for unmeasured (subscription) calls when
46
+ * `estimateUnmeasured` is on. A label ONLY — never counted toward `usd` or
47
+ * `usdMax`, never halts. Present only when > 0.
48
+ */
49
+ usdEstimated?: number;
50
+ /** True once total tokens >= tokensMax (false when no token limit). */
42
51
  breached: boolean;
52
+ /** True once measured usd >= usdMax (false when no USD limit). */
53
+ usdBreached: boolean;
43
54
  /** Whether the configured policy halts on breach (vs warn). */
44
55
  haltOnBreach: boolean;
45
56
  }
46
57
  export declare class RunBudget {
47
58
  private readonly tokensMax?;
59
+ private readonly usdMax?;
48
60
  private readonly haltOnBreach;
61
+ private readonly estimateUnmeasured;
62
+ private readonly priceTable?;
49
63
  private inputTokens;
50
64
  private outputTokens;
51
- /** Drivers that returned `usage: undefined` during this run, deduped. */
52
- private readonly unmeasuredDrivers;
65
+ private usdSpent;
66
+ /** Notional list-price USD for unmeasured calls (warn-only; never enforced). */
67
+ private usdEstimated;
68
+ /** Dedup keys for one-time warnings (unmeasured driver, breach, etc.). */
69
+ private readonly warnedKeys;
53
70
  /** Free-form warnings surfaced via the run log. */
54
71
  private readonly warningList;
55
- constructor(policy?: BudgetPolicy);
72
+ /**
73
+ * @param policy recipe budget block.
74
+ * @param priceTable optional injected table (tests); otherwise loaded once
75
+ * here when a USD cap is set (file/env override aware).
76
+ */
77
+ constructor(policy?: BudgetPolicy, priceTable?: PriceTable);
78
+ /** True when neither cap is configured — reconcile/admit are no-ops. */
79
+ private get hasBudget();
56
80
  /** Cheap admission check before dispatching an agent step. */
57
81
  admit(): BudgetAdmission;
58
82
  /**
59
- * Record the actual usage reported by an agent call. `usage` may be
60
- * undefined when the driver doesn't surface token counts — we record
61
- * the driver name once per run and continue (fail-open).
83
+ * Record the actual usage reported by an agent call. `usage` is undefined
84
+ * when the driver doesn't surface token counts — record the driver once and
85
+ * continue (fail-open). `model` is the resolved model (from `servedBy`) used
86
+ * to price USD; an unpriced model fails open with a one-time notice.
62
87
  */
63
- reconcile(driver: string, usage: AgentUsage | undefined): void;
88
+ reconcile(driver: string, usage: AgentUsage | undefined, model?: string, estimate?: {
89
+ inputChars: number;
90
+ outputChars: number;
91
+ }): void;
64
92
  /** Snapshot of current totals + breach state. */
65
93
  totals(): BudgetTotals;
66
94
  /** Warnings collected so the runner can surface them in the run log. */
67
95
  warnings(): string[];
96
+ /**
97
+ * Run-completion warnings: the live `warnings()` plus a one-line ≈$ summary
98
+ * of the notional cost estimated for unmeasured calls (only when
99
+ * `estimateUnmeasured` produced one). Use at run end; `warnings()` is the
100
+ * live per-driver list without the (end-of-run-only) summary.
101
+ */
102
+ finalWarnings(): string[];
103
+ /**
104
+ * USD remaining before the cap, or undefined when no USD cap is set.
105
+ * Cost-aware routing (Phase 4) uses this to decide when to downshift.
106
+ */
107
+ remainingUsd(): number | undefined;
108
+ /**
109
+ * Pre-dispatch USD estimate for a prospective (driver, model) call, or
110
+ * undefined when it would NOT be USD-enforced: no USD cap, a non-billable
111
+ * driver (local / subscription), or an unpriced model. Mirrors exactly the
112
+ * enforcement rule in `reconcile`, so a candidate the router treats as
113
+ * "free" is precisely one `reconcile` would not charge.
114
+ */
115
+ quoteUsd(driver: string | undefined, model: string | undefined, inputTokens: number, outputTokens: number): number | undefined;
116
+ private pushOnce;
68
117
  private totalTokens;
69
- private breached;
118
+ private tokenBreached;
119
+ private usdMaxBreached;
70
120
  }
@@ -1,83 +1,149 @@
1
1
  /**
2
- * Per-recipe token budget — PR2b.
2
+ * Per-recipe budget — PR2b (tokens) + cost-routing Phase 3 (USD).
3
3
  *
4
- * Built on PR2a's `AgentResult.usage` plumbing. The runner constructs
5
- * one `RunBudget` per recipe execution; agent steps consult it before
6
- * dispatch (admission) and reconcile actual consumption after the call
7
- * returns. On breach the run halts at the *next* admission check (we
8
- * never retroactively fail a step that succeeded — the user's tokens
9
- * are already spent, halting after-the-fact would just confuse the
10
- * audit trail).
4
+ * Built on PR2a's `AgentResult.usage` plumbing. The runner constructs one
5
+ * `RunBudget` per recipe execution; agent steps consult it before dispatch
6
+ * (admission) and reconcile actual consumption after the call returns. On
7
+ * breach the run halts at the *next* admission check (we never retroactively
8
+ * fail a step that succeeded — the user's tokens/dollars are already spent;
9
+ * halting after-the-fact would just confuse the audit trail).
11
10
  *
12
- * Subscription drivers (Claude CLI, provider subprocess CLIs) don't
13
- * surface per-call token counts. `RunBudget` tracks which drivers were
14
- * unmeasured and exposes a deduped warnings list fail-open with a
15
- * single notice per driver per run, never block.
11
+ * Two independent caps, either or both:
12
+ * - `tokensMax` cumulative input + output tokens.
13
+ * - `usdMax` — cumulative USD, priced from token usage via the Phase 2
14
+ * price table (`costUsd`).
16
15
  *
17
- * Scope (PR2b only):
18
- * - cumulative token enforcement (input + output)
19
- * - halt-on-breach OR warn-on-breach via `BudgetPolicy.onBreach`
20
- * - subscription-driver fail-open with warning
21
- *
22
- * Out of scope (future):
23
- * - per-step caps (extend `BudgetPolicy` or add `AgentStep.tokensMax`)
24
- * - `usdMax` (needs a price table; subscription drivers complicate)
25
- * - `wallClockMs` (orthogonal to tokens, separate accounting)
16
+ * Subscription drivers (Claude CLI, provider subprocess CLIs) don't surface
17
+ * per-call token counts, and a model with no price-table entry can't be
18
+ * costed. Both cases FAIL OPEN: `RunBudget` records a deduped one-time
19
+ * warning and never blocks. So a USD cap enforces for *measured + priced*
20
+ * (API) drivers and is a no-op-with-notice for everything else — never a
21
+ * silent or a surprise halt on the token-blind default driver. (The opt-in
22
+ * estimate-the-unmeasured path is a planned follow-up; absent it, unmeasured
23
+ * = fail-open, which is the conservative default the design calls for.)
24
+ */
25
+ import { DEFAULT_MODEL } from "./agentExecutor.js";
26
+ import { costUsd, loadPriceTable, } from "./pricing/priceTable.js";
27
+ /**
28
+ * Drivers that incur real, metered, per-token API billing — the ONLY ones a
29
+ * USD cap is enforced against. Includes the metered Gemini API driver
30
+ * (stamped "gemini"); the subscription/CLI subprocess drivers report no usage
31
+ * and never reach the pricing path; `local`
32
+ * (self-hosted Ollama / LM Studio) DOES report usage but costs no real money,
33
+ * so it must not be priced at notional API rates and halted on spend that
34
+ * never happened. Anything not in this set fails open with a one-time notice.
26
35
  */
36
+ const BILLABLE_DRIVERS = new Set(["anthropic", "openai", "grok", "gemini"]);
37
+ /** Rough chars-per-token used for the opt-in unmeasured-driver USD estimate. */
38
+ const ESTIMATE_CHARS_PER_TOKEN = 4;
27
39
  export class RunBudget {
28
40
  tokensMax;
41
+ usdMax;
29
42
  haltOnBreach;
43
+ estimateUnmeasured;
44
+ priceTable;
30
45
  inputTokens = 0;
31
46
  outputTokens = 0;
32
- /** Drivers that returned `usage: undefined` during this run, deduped. */
33
- unmeasuredDrivers = new Set();
47
+ usdSpent = 0;
48
+ /** Notional list-price USD for unmeasured calls (warn-only; never enforced). */
49
+ usdEstimated = 0;
50
+ /** Dedup keys for one-time warnings (unmeasured driver, breach, etc.). */
51
+ warnedKeys = new Set();
34
52
  /** Free-form warnings surfaced via the run log. */
35
53
  warningList = [];
36
- constructor(policy) {
54
+ /**
55
+ * @param policy recipe budget block.
56
+ * @param priceTable optional injected table (tests); otherwise loaded once
57
+ * here when a USD cap is set (file/env override aware).
58
+ */
59
+ constructor(policy, priceTable) {
37
60
  this.tokensMax = policy?.tokensMax;
61
+ this.usdMax = policy?.usdMax;
38
62
  this.haltOnBreach = (policy?.onBreach ?? "halt") === "halt";
63
+ this.estimateUnmeasured = policy?.estimateUnmeasured ?? false;
64
+ if (this.usdMax !== undefined) {
65
+ this.priceTable = priceTable ?? loadPriceTable();
66
+ }
67
+ }
68
+ /** True when neither cap is configured — reconcile/admit are no-ops. */
69
+ get hasBudget() {
70
+ return this.tokensMax !== undefined || this.usdMax !== undefined;
39
71
  }
40
72
  /** Cheap admission check before dispatching an agent step. */
41
73
  admit() {
42
- if (this.tokensMax === undefined)
74
+ // warn-mode never blocks; the breach warning was emitted at reconcile.
75
+ if (!this.haltOnBreach)
43
76
  return { admitted: true };
44
- if (this.breached()) {
45
- if (!this.haltOnBreach) {
46
- // warn-mode: never block, just keep going. The warning was
47
- // already emitted on the post-call reconcile that breached.
48
- return { admitted: true };
49
- }
77
+ if (this.tokenBreached()) {
50
78
  return {
51
79
  admitted: false,
52
- reason: `Run exceeded its token budget — budget_exceeded: total=${this.totalTokens()} > tokensMax=${this.tokensMax}.`,
80
+ reason: `Run exceeded its token budget — budget_exceeded: total=${this.totalTokens()} >= tokensMax=${this.tokensMax}.`,
81
+ };
82
+ }
83
+ if (this.usdMaxBreached()) {
84
+ return {
85
+ admitted: false,
86
+ reason: `Run exceeded its USD budget — budget_exceeded: usd=$${this.usdSpent.toFixed(4)} >= usdMax=$${this.usdMax}.`,
53
87
  };
54
88
  }
55
89
  return { admitted: true };
56
90
  }
57
91
  /**
58
- * Record the actual usage reported by an agent call. `usage` may be
59
- * undefined when the driver doesn't surface token counts — we record
60
- * the driver name once per run and continue (fail-open).
92
+ * Record the actual usage reported by an agent call. `usage` is undefined
93
+ * when the driver doesn't surface token counts — record the driver once and
94
+ * continue (fail-open). `model` is the resolved model (from `servedBy`) used
95
+ * to price USD; an unpriced model fails open with a one-time notice.
61
96
  */
62
- reconcile(driver, usage) {
63
- if (this.tokensMax === undefined)
97
+ reconcile(driver, usage, model, estimate) {
98
+ if (!this.hasBudget)
64
99
  return;
65
100
  if (!usage) {
66
- if (!this.unmeasuredDrivers.has(driver)) {
67
- this.unmeasuredDrivers.add(driver);
68
- this.warningList.push(`Driver "${driver}" does not report token usage budget enforcement skipped for its calls. Set recipe.budget.onBreach="warn" or move to an API driver to fix.`);
101
+ if (this.usdMax !== undefined && this.estimateUnmeasured && estimate) {
102
+ // OPT-IN notional estimate for an unmeasured (subscription) driver.
103
+ // WARN-ONLY: accumulates into usdEstimated, NEVER usdSpent, and never
104
+ // affects admit() — a flat-rate subscription call is not real money out,
105
+ // and halting on a ~4-chars/token guess would be wrong.
106
+ const estModel = model ?? DEFAULT_MODEL;
107
+ const estCost = costUsd(estModel, {
108
+ inputTokens: Math.ceil(estimate.inputChars / ESTIMATE_CHARS_PER_TOKEN),
109
+ outputTokens: Math.ceil(estimate.outputChars / ESTIMATE_CHARS_PER_TOKEN),
110
+ }, this.priceTable);
111
+ if (estCost !== undefined && Number.isFinite(estCost)) {
112
+ this.usdEstimated += estCost;
113
+ this.pushOnce(`estimate:${driver}`, `Driver "${driver}" reports no usage — estimating notional list-price cost (≈, never enforced; see usdEstimated).`);
114
+ return;
115
+ }
69
116
  }
117
+ this.pushOnce(`unmeasured:${driver}`, `Driver "${driver}" does not report token usage — budget enforcement skipped for its calls. Set recipe.budget.onBreach="warn" or move to an API driver to fix.`);
70
118
  return;
71
119
  }
72
120
  this.inputTokens += usage.inputTokens;
73
121
  this.outputTokens += usage.outputTokens;
74
- if (this.breached() && !this.haltOnBreach) {
75
- // warn-mode: emit a single in-band warning the first time we
76
- // cross the line. Subsequent steps continue running.
77
- const breachKey = "warn-breach-emitted";
78
- if (!this.unmeasuredDrivers.has(breachKey)) {
79
- this.unmeasuredDrivers.add(breachKey);
80
- this.warningList.push(`Token budget exceeded (total=${this.totalTokens()}, tokensMax=${this.tokensMax}) but onBreach="warn" — continuing.`);
122
+ if (this.usdMax !== undefined) {
123
+ if (!BILLABLE_DRIVERS.has(driver)) {
124
+ // Not a metered-API driver (local / subscription) — a USD cap here
125
+ // would be notional, not real money out. Fail open with a notice.
126
+ this.pushOnce(`notbilled:${driver}`, `Driver "${driver}" does not incur metered API cost — usdMax is not enforced for its calls.`);
127
+ }
128
+ else {
129
+ const cost = costUsd(model ?? "", usage, this.priceTable);
130
+ // `cost === undefined` (unpriced model) OR a non-finite cost (defends
131
+ // against a malformed price entry) → fail open, never poison usdSpent.
132
+ if (cost === undefined || !Number.isFinite(cost)) {
133
+ this.pushOnce(`unpriced:${model ?? "(no model)"}`, `Model "${model ?? "(unspecified)"}" is not in the price table — USD budget enforcement skipped for its calls. Add it to ~/.patchwork/prices.json or set the agent step's model.`);
134
+ }
135
+ else {
136
+ this.usdSpent += cost;
137
+ }
138
+ }
139
+ }
140
+ // warn-mode: emit a single in-band notice the first time we cross a cap.
141
+ if (!this.haltOnBreach) {
142
+ if (this.tokenBreached()) {
143
+ this.pushOnce("warn-token-breach", `Token budget exceeded (total=${this.totalTokens()}, tokensMax=${this.tokensMax}) but onBreach="warn" — continuing.`);
144
+ }
145
+ if (this.usdMaxBreached()) {
146
+ this.pushOnce("warn-usd-breach", `USD budget exceeded (usd=$${this.usdSpent.toFixed(4)}, usdMax=$${this.usdMax}) but onBreach="warn" — continuing.`);
81
147
  }
82
148
  }
83
149
  }
@@ -91,7 +157,13 @@ export class RunBudget {
91
157
  ...(this.tokensMax !== undefined && {
92
158
  remaining: Math.max(0, this.tokensMax - total),
93
159
  }),
94
- breached: this.breached(),
160
+ ...(this.usdMax !== undefined && {
161
+ usd: this.usdSpent,
162
+ usdRemaining: Math.max(0, this.usdMax - this.usdSpent),
163
+ }),
164
+ ...(this.usdEstimated > 0 && { usdEstimated: this.usdEstimated }),
165
+ breached: this.tokenBreached(),
166
+ usdBreached: this.usdMaxBreached(),
95
167
  haltOnBreach: this.haltOnBreach,
96
168
  };
97
169
  }
@@ -99,11 +171,72 @@ export class RunBudget {
99
171
  warnings() {
100
172
  return [...this.warningList];
101
173
  }
174
+ /**
175
+ * Run-completion warnings: the live `warnings()` plus a one-line ≈$ summary
176
+ * of the notional cost estimated for unmeasured calls (only when
177
+ * `estimateUnmeasured` produced one). Use at run end; `warnings()` is the
178
+ * live per-driver list without the (end-of-run-only) summary.
179
+ */
180
+ finalWarnings() {
181
+ const out = [...this.warningList];
182
+ if (this.usdEstimated > 0) {
183
+ out.push(`Unmeasured (subscription) calls would cost ≈$${this.usdEstimated.toFixed(4)} at list prices — estimate only, never enforced.`);
184
+ }
185
+ return out;
186
+ }
187
+ /**
188
+ * USD remaining before the cap, or undefined when no USD cap is set.
189
+ * Cost-aware routing (Phase 4) uses this to decide when to downshift.
190
+ */
191
+ remainingUsd() {
192
+ if (this.usdMax === undefined)
193
+ return undefined;
194
+ return Math.max(0, this.usdMax - this.usdSpent);
195
+ }
196
+ /**
197
+ * Pre-dispatch USD estimate for a prospective (driver, model) call, or
198
+ * undefined when it would NOT be USD-enforced: no USD cap, a non-billable
199
+ * driver (local / subscription), or an unpriced model. Mirrors exactly the
200
+ * enforcement rule in `reconcile`, so a candidate the router treats as
201
+ * "free" is precisely one `reconcile` would not charge.
202
+ */
203
+ quoteUsd(driver, model, inputTokens, outputTokens) {
204
+ if (this.usdMax === undefined)
205
+ return undefined;
206
+ // Mirror executeAgent/reconcile resolution so a candidate the router calls
207
+ // "free" is EXACTLY one reconcile would not charge:
208
+ // - "api"/"claude" select the billable Anthropic path (→ "anthropic").
209
+ // - undefined optimistically assumes auto-detect lands on a metered API
210
+ // driver (usually anthropic); if it actually resolves to a subscription
211
+ // driver the call is free and the cap simply isn't enforced there.
212
+ const resolvedDriver = driver === "api" || driver === "claude" ? "anthropic" : driver;
213
+ if (resolvedDriver !== undefined && !BILLABLE_DRIVERS.has(resolvedDriver)) {
214
+ return undefined;
215
+ }
216
+ // The anthropic path bills DEFAULT_MODEL when the step omits `model`.
217
+ // (Provider drivers default internally to a model unknowable pre-dispatch,
218
+ // so an omitted model there stays unpriced → not routed on.)
219
+ const onAnthropicPath = resolvedDriver === "anthropic" || resolvedDriver === undefined;
220
+ const resolvedModel = model ?? (onAnthropicPath ? DEFAULT_MODEL : undefined);
221
+ if (!resolvedModel)
222
+ return undefined;
223
+ const cost = costUsd(resolvedModel, { inputTokens, outputTokens }, this.priceTable);
224
+ return cost !== undefined && Number.isFinite(cost) ? cost : undefined;
225
+ }
226
+ pushOnce(key, msg) {
227
+ if (this.warnedKeys.has(key))
228
+ return;
229
+ this.warnedKeys.add(key);
230
+ this.warningList.push(msg);
231
+ }
102
232
  totalTokens() {
103
233
  return this.inputTokens + this.outputTokens;
104
234
  }
105
- breached() {
235
+ tokenBreached() {
106
236
  return this.tokensMax !== undefined && this.totalTokens() >= this.tokensMax;
107
237
  }
238
+ usdMaxBreached() {
239
+ return this.usdMax !== undefined && this.usdSpent >= this.usdMax;
240
+ }
108
241
  }
109
242
  //# sourceMappingURL=runBudget.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"runBudget.js","sourceRoot":"","sources":["../../src/recipes/runBudget.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runBudget.js","sourceRoot":"","sources":["../../src/recipes/runBudget.ts"],"names":[],"mappings":"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"}
@@ -115,20 +115,41 @@ export interface ErrorPolicy {
115
115
  * drivers (Anthropic API, OpenAI/Gemini/Grok subprocess that surfaces
116
116
  * usage, local LLM adapters) get full enforcement.
117
117
  *
118
- * Nested object (not flat `tokensMax`) so future siblings — `usdMax`,
119
- * `wallClockMs`, `stepsMax` — don't churn the schema again.
118
+ * Nested object (not flat `tokensMax`) so siblings — `usdMax` (added in
119
+ * cost-routing Phase 3), and future `wallClockMs` / `stepsMax` — don't churn
120
+ * the schema again.
120
121
  */
121
122
  export interface BudgetPolicy {
122
123
  /** Cumulative input + output tokens allowed across the whole run. */
123
124
  tokensMax?: number;
124
125
  /**
125
- * What to do when the budget is breached. `halt` (default) stops the
126
+ * Cumulative USD allowed across the whole run, priced from token usage via
127
+ * the model price table (`src/recipes/pricing`). Enforced for measured +
128
+ * priced (API) drivers; a subscription driver that reports no tokens, or a
129
+ * model with no price-table entry, FAILS OPEN with a one-time warning and
130
+ * never halts on it. Same `onBreach` semantics as `tokensMax`. A USD cap on
131
+ * a subscription driver would be notional, not real money out — so it is
132
+ * deliberately not enforced there.
133
+ */
134
+ usdMax?: number;
135
+ /**
136
+ * What to do when a budget is breached. `halt` (default) stops the
126
137
  * run on the next admission check with a `budget_exceeded` halt
127
138
  * reason. `warn` continues the run but emits a warning + records the
128
139
  * breach in the run log; useful for tuning budgets without breaking
129
- * production cron recipes.
140
+ * production cron recipes. Applies to both `tokensMax` and `usdMax`.
130
141
  */
131
142
  onBreach?: "halt" | "warn";
143
+ /**
144
+ * OPT-IN, default **false**. When true, the runner ESTIMATES the notional USD
145
+ * a subscription/unmeasured driver call would have cost at list prices (from
146
+ * the prompt + output length) and surfaces a `≈$X` figure. This is a label
147
+ * ONLY — estimated spend is tracked separately from `usdMax` and can NEVER
148
+ * halt a run (subscription spend is flat-rate, not real money out; halting on
149
+ * a ~4-chars/token guess would be wrong). Requires `usdMax` to be set (it
150
+ * reuses the price table). Default false preserves the fail-open invariant.
151
+ */
152
+ estimateUnmeasured?: boolean;
132
153
  }
133
154
  export interface Recipe {
134
155
  name: string;
@@ -22,6 +22,15 @@ export declare function generateSchemaSet(): SchemaSet;
22
22
  * should pin on `schemaVersion` and validate against this.
23
23
  */
24
24
  export declare function generateDryRunPlanSchema(): unknown;
25
+ /**
26
+ * JSON Schema for the What-If Preview `RecipeSimulationReport`
27
+ * (`src/recipes/simulation/types.ts`), the output of
28
+ * `GET /recipes/:name/simulate` and `patchwork recipe simulate`. A superset of
29
+ * the dry-run plan; consumers pin on `schemaVersion`. Standalone (not part of
30
+ * `generateSchemaSet`/`writeSchemas`) — served on demand to consumers that want
31
+ * to validate the wire contract.
32
+ */
33
+ export declare function generateSimulationSchema(): unknown;
25
34
  /**
26
35
  * Write schema files to disk.
27
36
  */