@wiro-ai/wiro-mcp 1.4.1 → 1.5.0

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 (226) hide show
  1. package/README.md +38 -15
  2. package/dist/client.d.ts +19 -6
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +44 -27
  5. package/dist/client.js.map +1 -1
  6. package/dist/instructions.d.ts.map +1 -1
  7. package/dist/instructions.js +7 -4
  8. package/dist/instructions.js.map +1 -1
  9. package/dist/playbooks.d.ts.map +1 -1
  10. package/dist/playbooks.js +45 -34
  11. package/dist/playbooks.js.map +1 -1
  12. package/dist/prompts.d.ts.map +1 -1
  13. package/dist/prompts.js +3 -2
  14. package/dist/prompts.js.map +1 -1
  15. package/dist/tools/apply-agent-skills.d.ts.map +1 -1
  16. package/dist/tools/apply-agent-skills.js +343 -87
  17. package/dist/tools/apply-agent-skills.js.map +1 -1
  18. package/dist/tools/bind-agent-project.d.ts +4 -0
  19. package/dist/tools/bind-agent-project.d.ts.map +1 -0
  20. package/dist/tools/bind-agent-project.js +403 -0
  21. package/dist/tools/bind-agent-project.js.map +1 -0
  22. package/dist/tools/cancel-agent-message.d.ts.map +1 -1
  23. package/dist/tools/cancel-agent-message.js +6 -3
  24. package/dist/tools/cancel-agent-message.js.map +1 -1
  25. package/dist/tools/cancel-task.js +3 -3
  26. package/dist/tools/cancel-task.js.map +1 -1
  27. package/dist/tools/clone-workflow.d.ts.map +1 -1
  28. package/dist/tools/clone-workflow.js +5 -4
  29. package/dist/tools/clone-workflow.js.map +1 -1
  30. package/dist/tools/connect-agent-oauth.d.ts.map +1 -1
  31. package/dist/tools/connect-agent-oauth.js +140 -40
  32. package/dist/tools/connect-agent-oauth.js.map +1 -1
  33. package/dist/tools/control-agent.d.ts.map +1 -1
  34. package/dist/tools/control-agent.js +109 -36
  35. package/dist/tools/control-agent.js.map +1 -1
  36. package/dist/tools/create-workflow.d.ts +3 -0
  37. package/dist/tools/create-workflow.d.ts.map +1 -1
  38. package/dist/tools/create-workflow.js +12 -11
  39. package/dist/tools/create-workflow.js.map +1 -1
  40. package/dist/tools/delete-agent-session.d.ts.map +1 -1
  41. package/dist/tools/delete-agent-session.js +18 -12
  42. package/dist/tools/delete-agent-session.js.map +1 -1
  43. package/dist/tools/delete-agent.d.ts.map +1 -1
  44. package/dist/tools/delete-agent.js +12 -9
  45. package/dist/tools/delete-agent.js.map +1 -1
  46. package/dist/tools/delete-workflow.d.ts.map +1 -1
  47. package/dist/tools/delete-workflow.js +5 -4
  48. package/dist/tools/delete-workflow.js.map +1 -1
  49. package/dist/tools/deploy-agent.d.ts.map +1 -1
  50. package/dist/tools/deploy-agent.js +200 -40
  51. package/dist/tools/deploy-agent.js.map +1 -1
  52. package/dist/tools/disconnect-agent-oauth.d.ts.map +1 -1
  53. package/dist/tools/disconnect-agent-oauth.js +32 -12
  54. package/dist/tools/disconnect-agent-oauth.js.map +1 -1
  55. package/dist/tools/get-account.d.ts.map +1 -1
  56. package/dist/tools/get-account.js +148 -42
  57. package/dist/tools/get-account.js.map +1 -1
  58. package/dist/tools/get-agent-activity.d.ts.map +1 -1
  59. package/dist/tools/get-agent-activity.js +185 -35
  60. package/dist/tools/get-agent-activity.js.map +1 -1
  61. package/dist/tools/get-agent-conversation.d.ts.map +1 -1
  62. package/dist/tools/get-agent-conversation.js +57 -35
  63. package/dist/tools/get-agent-conversation.js.map +1 -1
  64. package/dist/tools/get-agent-message.d.ts.map +1 -1
  65. package/dist/tools/get-agent-message.js +6 -4
  66. package/dist/tools/get-agent-message.js.map +1 -1
  67. package/dist/tools/get-agent-template.d.ts.map +1 -1
  68. package/dist/tools/get-agent-template.js +11 -16
  69. package/dist/tools/get-agent-template.js.map +1 -1
  70. package/dist/tools/get-model-schema.js +3 -3
  71. package/dist/tools/get-model-schema.js.map +1 -1
  72. package/dist/tools/get-my-agent.d.ts.map +1 -1
  73. package/dist/tools/get-my-agent.js +11 -7
  74. package/dist/tools/get-my-agent.js.map +1 -1
  75. package/dist/tools/get-playbook.d.ts.map +1 -1
  76. package/dist/tools/get-playbook.js +3 -2
  77. package/dist/tools/get-playbook.js.map +1 -1
  78. package/dist/tools/get-task-price.js +3 -3
  79. package/dist/tools/get-task-price.js.map +1 -1
  80. package/dist/tools/get-task.js +3 -3
  81. package/dist/tools/get-task.js.map +1 -1
  82. package/dist/tools/get-workflow.d.ts.map +1 -1
  83. package/dist/tools/get-workflow.js +38 -19
  84. package/dist/tools/get-workflow.js.map +1 -1
  85. package/dist/tools/index.d.ts +3 -1
  86. package/dist/tools/index.d.ts.map +1 -1
  87. package/dist/tools/index.js +6 -1
  88. package/dist/tools/index.js.map +1 -1
  89. package/dist/tools/kill-task.js +3 -3
  90. package/dist/tools/kill-task.js.map +1 -1
  91. package/dist/tools/list-agent-skills.d.ts.map +1 -1
  92. package/dist/tools/list-agent-skills.js +5 -4
  93. package/dist/tools/list-agent-skills.js.map +1 -1
  94. package/dist/tools/list-agent-templates.js +4 -4
  95. package/dist/tools/list-agent-templates.js.map +1 -1
  96. package/dist/tools/list-my-agents.d.ts.map +1 -1
  97. package/dist/tools/list-my-agents.js +11 -8
  98. package/dist/tools/list-my-agents.js.map +1 -1
  99. package/dist/tools/list-projects.d.ts +4 -0
  100. package/dist/tools/list-projects.d.ts.map +1 -0
  101. package/dist/tools/list-projects.js +141 -0
  102. package/dist/tools/list-projects.js.map +1 -0
  103. package/dist/tools/list-studio-results.js +3 -3
  104. package/dist/tools/list-studio-results.js.map +1 -1
  105. package/dist/tools/list-tasks.js +3 -3
  106. package/dist/tools/list-tasks.js.map +1 -1
  107. package/dist/tools/list-workflow-runs.d.ts.map +1 -1
  108. package/dist/tools/list-workflow-runs.js +6 -4
  109. package/dist/tools/list-workflow-runs.js.map +1 -1
  110. package/dist/tools/list-workflows.d.ts.map +1 -1
  111. package/dist/tools/list-workflows.js +7 -6
  112. package/dist/tools/list-workflows.js.map +1 -1
  113. package/dist/tools/manage-agent-billing.d.ts.map +1 -1
  114. package/dist/tools/manage-agent-billing.js +207 -59
  115. package/dist/tools/manage-agent-billing.js.map +1 -1
  116. package/dist/tools/manage-agent-custom-skill.d.ts.map +1 -1
  117. package/dist/tools/manage-agent-custom-skill.js +88 -12
  118. package/dist/tools/manage-agent-custom-skill.js.map +1 -1
  119. package/dist/tools/recommend-model.js +3 -3
  120. package/dist/tools/recommend-model.js.map +1 -1
  121. package/dist/tools/resume-workflow-run.d.ts.map +1 -1
  122. package/dist/tools/resume-workflow-run.js +3 -2
  123. package/dist/tools/resume-workflow-run.js.map +1 -1
  124. package/dist/tools/run-model.js +3 -3
  125. package/dist/tools/run-model.js.map +1 -1
  126. package/dist/tools/run-workflow.d.ts.map +1 -1
  127. package/dist/tools/run-workflow.js +3 -2
  128. package/dist/tools/run-workflow.js.map +1 -1
  129. package/dist/tools/save-workflow-from-agent.d.ts.map +1 -1
  130. package/dist/tools/save-workflow-from-agent.js +17 -12
  131. package/dist/tools/save-workflow-from-agent.js.map +1 -1
  132. package/dist/tools/schemas.d.ts +32 -26
  133. package/dist/tools/schemas.d.ts.map +1 -1
  134. package/dist/tools/schemas.js +67 -0
  135. package/dist/tools/schemas.js.map +1 -1
  136. package/dist/tools/search-docs.js +3 -3
  137. package/dist/tools/search-docs.js.map +1 -1
  138. package/dist/tools/search-models.js +3 -3
  139. package/dist/tools/search-models.js.map +1 -1
  140. package/dist/tools/send-agent-message.d.ts.map +1 -1
  141. package/dist/tools/send-agent-message.js +30 -9
  142. package/dist/tools/send-agent-message.js.map +1 -1
  143. package/dist/tools/set-agent-credentials.d.ts.map +1 -1
  144. package/dist/tools/set-agent-credentials.js +47 -57
  145. package/dist/tools/set-agent-credentials.js.map +1 -1
  146. package/dist/tools/update-agent.d.ts.map +1 -1
  147. package/dist/tools/update-agent.js +56 -19
  148. package/dist/tools/update-agent.js.map +1 -1
  149. package/dist/tools/update-workflow.d.ts +199 -0
  150. package/dist/tools/update-workflow.d.ts.map +1 -1
  151. package/dist/tools/update-workflow.js +20 -7
  152. package/dist/tools/update-workflow.js.map +1 -1
  153. package/dist/tools/upload-file.js +3 -3
  154. package/dist/tools/upload-file.js.map +1 -1
  155. package/dist/tools/wait-for-task.js +3 -3
  156. package/dist/tools/wait-for-task.js.map +1 -1
  157. package/dist/tools/wait-for-workflow-run.d.ts +3 -3
  158. package/dist/tools/wait-for-workflow-run.d.ts.map +1 -1
  159. package/dist/tools/wait-for-workflow-run.js +7 -6
  160. package/dist/tools/wait-for-workflow-run.js.map +1 -1
  161. package/dist/utils/agent-catalog.d.ts +29 -0
  162. package/dist/utils/agent-catalog.d.ts.map +1 -1
  163. package/dist/utils/agent-catalog.js +28 -7
  164. package/dist/utils/agent-catalog.js.map +1 -1
  165. package/dist/utils/agent-config.d.ts +109 -8
  166. package/dist/utils/agent-config.d.ts.map +1 -1
  167. package/dist/utils/agent-config.js +286 -45
  168. package/dist/utils/agent-config.js.map +1 -1
  169. package/dist/utils/agent-message.d.ts +36 -14
  170. package/dist/utils/agent-message.d.ts.map +1 -1
  171. package/dist/utils/agent-message.js +92 -26
  172. package/dist/utils/agent-message.js.map +1 -1
  173. package/dist/utils/agent-targets.d.ts +7 -3
  174. package/dist/utils/agent-targets.d.ts.map +1 -1
  175. package/dist/utils/agent-targets.js +35 -23
  176. package/dist/utils/agent-targets.js.map +1 -1
  177. package/dist/utils/agent-turns.d.ts +19 -12
  178. package/dist/utils/agent-turns.d.ts.map +1 -1
  179. package/dist/utils/agent-turns.js +97 -15
  180. package/dist/utils/agent-turns.js.map +1 -1
  181. package/dist/utils/api.d.ts +6 -0
  182. package/dist/utils/api.d.ts.map +1 -1
  183. package/dist/utils/api.js +22 -1
  184. package/dist/utils/api.js.map +1 -1
  185. package/dist/utils/format.d.ts.map +1 -1
  186. package/dist/utils/format.js +7 -13
  187. package/dist/utils/format.js.map +1 -1
  188. package/dist/utils/json-schema-dialect.d.ts.map +1 -1
  189. package/dist/utils/json-schema-dialect.js +4 -0
  190. package/dist/utils/json-schema-dialect.js.map +1 -1
  191. package/dist/utils/media.d.ts +8 -0
  192. package/dist/utils/media.d.ts.map +1 -1
  193. package/dist/utils/media.js +29 -0
  194. package/dist/utils/media.js.map +1 -1
  195. package/dist/utils/poll.d.ts +5 -2
  196. package/dist/utils/poll.d.ts.map +1 -1
  197. package/dist/utils/poll.js +10 -9
  198. package/dist/utils/poll.js.map +1 -1
  199. package/dist/utils/quoted-text.d.ts +28 -0
  200. package/dist/utils/quoted-text.d.ts.map +1 -0
  201. package/dist/utils/quoted-text.js +64 -0
  202. package/dist/utils/quoted-text.js.map +1 -0
  203. package/dist/utils/studio-runs.d.ts.map +1 -1
  204. package/dist/utils/studio-runs.js +7 -4
  205. package/dist/utils/studio-runs.js.map +1 -1
  206. package/dist/utils/user-agent.d.ts +101 -25
  207. package/dist/utils/user-agent.d.ts.map +1 -1
  208. package/dist/utils/user-agent.js +160 -42
  209. package/dist/utils/user-agent.js.map +1 -1
  210. package/dist/utils/wiro-project.d.ts +45 -0
  211. package/dist/utils/wiro-project.d.ts.map +1 -0
  212. package/dist/utils/wiro-project.js +66 -0
  213. package/dist/utils/wiro-project.js.map +1 -0
  214. package/dist/utils/workflow-save.d.ts +22 -1
  215. package/dist/utils/workflow-save.d.ts.map +1 -1
  216. package/dist/utils/workflow-save.js +122 -24
  217. package/dist/utils/workflow-save.js.map +1 -1
  218. package/dist/utils/workflow.d.ts +26 -13
  219. package/dist/utils/workflow.d.ts.map +1 -1
  220. package/dist/utils/workflow.js +51 -20
  221. package/dist/utils/workflow.js.map +1 -1
  222. package/dist/utils/workspace.d.ts +30 -4
  223. package/dist/utils/workspace.d.ts.map +1 -1
  224. package/dist/utils/workspace.js +95 -12
  225. package/dist/utils/workspace.js.map +1 -1
  226. package/package.json +1 -1
@@ -1,17 +1,27 @@
1
1
  import { z } from 'zod';
2
2
  import { asString, errorResult, errorText, isPlainObject, requireApiSuccess, toNumber, unixToIso, } from '../utils/api.js';
3
- import { agentStatusLabel, agentStatusOf, checkSkillSet, credentialsNeededFor, describeSkillsApplyError, diffSkills, enabledSkillNames, estimateProratedUsd, formatUsd, isAgentRunning, loadSkillRegistry, newSkillChangeKey, parseSkillChangeKey, planBilling, planSkillChanges, readUserAgent, restartNote, skillChangeDigest, skillPriceTag, workspaceHint, } from '../utils/agent-config.js';
4
- import { asTier, outcomeUnknown, toTierPricing } from '../utils/user-agent.js';
3
+ import { agentStatusLabel, agentStatusOf, checkSkillSet, credentialsNeededFor, daysLeftText, describeSkillsApplyError, diffSkills, enabledSkillNames, formatUsd, isAgentRunning, loadSkillRegistry, newSkillChangeKey, parseSkillChangeKey, planBilling, planSkillChanges, readPlanChange, readUserAgent, underOneDayLeft, restartNote, skillChangeDigest, skillPriceTag, workspaceHint, } from '../utils/agent-config.js';
4
+ import { asTier, bindProjectStep, formatCredits, outcomeUnknown, toTierPricing, WIRO_PROJECT_CREDENTIAL } from '../utils/user-agent.js';
5
5
  import { DEFAULT_WAIT_SECONDS } from '../utils/poll.js';
6
6
  import { isPlainTeamMember, teamAgentBillingRefused } from '../utils/workspace.js';
7
- import { nextActionSchema } from './schemas.js';
7
+ import { nextActionSchema, strictInput } from './schemas.js';
8
8
  const pricingSchema = z.object({
9
9
  currentMonthlyUsd: z.number().nullable(),
10
10
  newMonthlyUsd: z.number().nullable(),
11
11
  deltaMonthlyUsd: z.number().nullable(),
12
- currentMonthlyCredits: z.number().nullable(),
13
- newMonthlyCredits: z.number().nullable(),
12
+ currentMonthlyCredits: z.number().nullable()
13
+ .describe('The monthly credits each renewal grants now.'),
14
+ newMonthlyCredits: z.number().nullable()
15
+ .describe('The monthly credits each renewal grants after the change.'),
14
16
  deltaMonthlyCredits: z.number().nullable(),
17
+ creditsNow: z.number().optional()
18
+ .describe('The credits this period gains now, below 0 when it loses them: the change counts for the time left in the period.'),
19
+ creditsThisPeriod: z.number().optional()
20
+ .describe('The monthly credits this period grants in all after the change.'),
21
+ creditsFromRenewal: z.number().optional()
22
+ .describe('The monthly credits every renewal grants after the change.'),
23
+ periodEnd: z.string().optional()
24
+ .describe('When the current period ends.'),
15
25
  note: z.string().optional(),
16
26
  });
17
27
  const outputSchema = {
@@ -34,7 +44,12 @@ const outputSchema = {
34
44
  plan: z.enum(['prepaid', 'card', 'none']),
35
45
  subscriptionStatus: z.string().nullable(),
36
46
  wallet: z.enum(['team', 'personal']),
37
- estimatedChargeNowUsd: z.number().nullable().optional(),
47
+ estimatedChargeNowUsd: z.number().nullable().optional()
48
+ .describe('What a wallet plan pays now at list price, as Wiro works it out, before any discount on the wallet; 0 on a card plan, whose difference goes to the card subscription.'),
49
+ creditNowUsd: z.number().nullable().optional()
50
+ .describe('For a drop on a wallet plan, what goes back to the balances that paid; null when it is worked out when the change is applied.'),
51
+ usedCreditsUsd: z.number().nullable().optional()
52
+ .describe('The part of a credit that comes off for credits already used above what the plan keeps.'),
38
53
  chargedNowUsd: z.number().nullable().optional(),
39
54
  note: z.string(),
40
55
  }),
@@ -56,9 +71,64 @@ const outputSchema = {
56
71
  nextAction: nextActionSchema.optional(),
57
72
  };
58
73
  const UNCHANGED_RESTART = 'Nothing changed, so the agent does not restart.';
59
- // A credit for skills turned off goes to the wallet with an end date: it can be
60
- // spent until the plan's current period ends.
61
- const CREDIT_SPEND_BY = 'It can be spent until this period ends.';
74
+ // The tier and the price live with Wiro, and the agent reads neither: only a skill that turns on or off restarts it.
75
+ const TIER_ONLY_RESTART = 'No skill turns on or off, so a change of the tier alone does not restart the agent.';
76
+ function roundCents(usd) {
77
+ return Math.round(usd * 100) / 100;
78
+ }
79
+ /**
80
+ * How long an applied credit lasts, from the parts Wiro says it split into. Each
81
+ * part went back to the balance that paid it and keeps that balance's end date,
82
+ * except the part whose source has ended since (each ...UntilPeriodEndUsd): that
83
+ * part lasts until the plan's current period ends. The two amounts add up to the
84
+ * credit, and parts that come to nothing say nothing.
85
+ */
86
+ function creditTerms(parts) {
87
+ if (!isPlainObject(parts))
88
+ return '';
89
+ const usd = (key) => toNumber(parts[key]) ?? 0;
90
+ const untilEnd = roundCents(usd('couponUntilPeriodEndUsd') + usd('storeUntilPeriodEndUsd') + usd('depositUntilPeriodEndUsd'));
91
+ const keepsEnd = roundCents(roundCents(usd('couponUsd') + usd('storeUsd') + usd('depositUsd')) - untilEnd);
92
+ if (keepsEnd > 0 && untilEnd > 0) {
93
+ return `${formatUsd(keepsEnd)} of it went back to the balances that paid and keeps their end dates, and ${formatUsd(untilEnd)} lasts until this period ends, because the credit that paid it has ended.`;
94
+ }
95
+ if (keepsEnd > 0)
96
+ return 'It went back to the balances that paid and keeps their end dates.';
97
+ return untilEnd > 0 ? 'It lasts until this period ends, because the credit that paid it has ended.' : '';
98
+ }
99
+ /** A count of credits with its sign, such as +97 or -690. */
100
+ function signedCredits(credits) {
101
+ return `${credits > 0 ? '+' : ''}${formatCredits(credits)}`;
102
+ }
103
+ /** The fields of Wiro's plan change that the pricing output carries. */
104
+ function periodFields(change) {
105
+ if (!change)
106
+ return {};
107
+ const periodEnd = unixToIso(change.periodEnd);
108
+ return {
109
+ ...(change.creditsNow !== null ? { creditsNow: change.creditsNow } : {}),
110
+ ...(change.creditsThisPeriod !== null ? { creditsThisPeriod: change.creditsThisPeriod } : {}),
111
+ ...(change.creditsFromRenewal !== null ? { creditsFromRenewal: change.creditsFromRenewal } : {}),
112
+ ...(periodEnd ? { periodEnd } : {}),
113
+ };
114
+ }
115
+ /**
116
+ * What a change does to this period's credits when none come with it: those it takes away end for the
117
+ * time left, and the period keeps the rest. A plan priced again at today's rates can also keep its
118
+ * credits, or gain some, while its price drops.
119
+ */
120
+ function creditsEndNote(skillsOff, creditsNow, creditsThisPeriod) {
121
+ if (creditsNow !== null && creditsNow > 0) {
122
+ return `This period gains ${formatCredits(creditsNow)} credits for the time left${creditsThisPeriod !== null ? `, ${formatCredits(creditsThisPeriod)} in all` : ''}.`;
123
+ }
124
+ if (creditsNow === 0) {
125
+ return creditsThisPeriod !== null ? `This period keeps its ${formatCredits(creditsThisPeriod)} credits.` : 'This period keeps its credits.';
126
+ }
127
+ const keeps = creditsThisPeriod !== null ? `, and this period keeps ${formatCredits(creditsThisPeriod)}` : '';
128
+ if (skillsOff === 0)
129
+ return `The credits the change takes away end now for the time left${keeps}.`;
130
+ return `${skillsOff === 1 ? 'Its' : 'Their'} credits for the time left end now${keeps}.`;
131
+ }
62
132
  function difference(next, current) {
63
133
  return next === null || current === null ? null : Math.round((next - current) * 100) / 100;
64
134
  }
@@ -66,9 +136,10 @@ function difference(next, current) {
66
136
  * A skill change prices the plan at today's prices for all its skills, so a
67
137
  * plan that started at other prices also changes for the skills it already
68
138
  * has, and so do its monthly credits. The agent's tiers carry today's prices
69
- * and credits for those skills. An agent without a plan has no price of its
70
- * own to move from, and a move to Pro prices the whole plan at today's Pro
71
- * prices, which the quote already shows.
139
+ * and credits for those skills. They are the base of the new price, not a price
140
+ * the plan reaches. An agent without an active plan cannot take the change yet,
141
+ * and a move to Pro prices the whole plan at today's Pro prices, which the quote
142
+ * already shows.
72
143
  */
73
144
  function todayPriceNote(row, billing, currentPrice, currentCredits, tierChange) {
74
145
  if (tierChange)
@@ -76,12 +147,12 @@ function todayPriceNote(row, billing, currentPrice, currentCredits, tierChange)
76
147
  const tier = asTier(row.tier);
77
148
  const tiers = toTierPricing(isPlainObject(row.agent) ? row.agent.tiers : undefined);
78
149
  const today = tier && tiers ? tiers[tier] : null;
79
- if (billing.plan === 'none' || currentPrice === null || today === null || !difference(today.priceUsd, currentPrice))
150
+ if (billing.status !== 'active' || currentPrice === null || today === null || !difference(today.priceUsd, currentPrice))
80
151
  return null;
81
- const credits = currentCredits !== null && today.monthlyCredits !== currentCredits
82
- ? `, and its monthly credits from ${currentCredits} to ${today.monthlyCredits}`
83
- : '';
84
- return `The change also moves the plan from its current ${formatUsd(currentPrice)} a month to ${formatUsd(today.priceUsd)} a month, today's ${tier === 'pro' ? 'Pro' : 'Starter'} price for the skills it already has${credits}.`;
152
+ const versus = currentCredits !== null && today.monthlyCredits !== currentCredits
153
+ ? ` with ${formatCredits(today.monthlyCredits)} monthly credits, not its current ${formatUsd(currentPrice)} and ${formatCredits(currentCredits)}`
154
+ : `, not its current ${formatUsd(currentPrice)}`;
155
+ return `Today's ${tier === 'pro' ? 'Pro' : 'Starter'} price for the skills this plan already has is ${formatUsd(today.priceUsd)} a month${versus}. A skill change counts from today's price, and the new price in this quote already includes that.`;
85
156
  }
86
157
  /**
87
158
  * The wallet of the agent's workspace, which Wiro charges or credits for a
@@ -92,56 +163,175 @@ function todayPriceNote(row, billing, currentPrice, currentCredits, tierChange)
92
163
  function walletName(billing) {
93
164
  return billing.wallet === 'team' ? 'the team wallet' : 'your personal wallet';
94
165
  }
95
- /** For a cancelled plan, a skill change only covers the days left: nothing renews at the new price. */
166
+ /** When the new monthly credits come: a plan set to end renews only if it is kept, and an agent without a plan gets them once it is subscribed. */
167
+ function creditsWhen(billing) {
168
+ if (billing.plan === 'none')
169
+ return 'a month once it is subscribed';
170
+ return billing.status === 'active' && billing.cancelAtPeriodEnd === true
171
+ ? 'at each renewal after the change, if the plan is kept'
172
+ : 'at each renewal after the change';
173
+ }
174
+ /** When an active plan's next renewal gives the new monthly credits. A card plan whose renewal is not known names it as a condition. */
175
+ function renewalWhen(billing) {
176
+ if (billing.cancelAtPeriodEnd === true)
177
+ return 'from the next renewal, if the plan is kept';
178
+ return billing.cancelAtPeriodEnd === null ? 'from the next renewal, if the plan renews' : 'from the next renewal';
179
+ }
180
+ /** What renewals give after an applied change: a plan that may not renew names it as a condition, as its quote did. */
181
+ function renewalGives(billing, credits) {
182
+ return billing.cancelAtPeriodEnd === false ? `every renewal gives ${formatCredits(credits)}` : `${formatCredits(credits)} ${renewalWhen(billing)}`;
183
+ }
184
+ /** For a cancelled plan, a skill change only covers the time left: nothing renews at the new price. */
96
185
  function cancelledNote(billing) {
97
186
  const end = billing.periodEnd !== null ? unixToIso(billing.periodEnd) : undefined;
98
187
  if (billing.plan === 'card') {
99
188
  return `The plan is set to end${end ? ` after ${end}` : ' with this period'} and does not renew.`;
100
189
  }
101
- return `The plan is cancelled and ends about a day after ${end ?? 'the end of this period'}, so this covers only the days left and the new price is not renewed. Undo the cancellation with manage_agent_billing (renew) to keep the plan.`;
190
+ return `The plan is cancelled and ends about a day after ${end ?? 'the end of this period'}, so this covers only the time left and the new price is not renewed. Undo the cancellation with manage_agent_billing (renew) to keep the plan.`;
191
+ }
192
+ /** A card subscription that ends with this period: the plan is set to end, or it moves to wallet billing. */
193
+ function cardEndsWithPeriod(billing) {
194
+ return billing.plan === 'card' && (billing.cancelAtPeriodEnd === true || billing.switchingToWallet);
102
195
  }
103
- function quoteBillingNote(billing, delta, estimate, nextPrice) {
196
+ function walletSwitchNote(billing, price) {
197
+ return `When this period ends, the plan moves to wallet billing and renews from ${walletName(billing)}${price !== null ? ` at ${formatUsd(price)} a month` : ''}.`;
198
+ }
199
+ // A card subscription that ends with this period is not renewed by card, so its notes point to no next invoice.
200
+ const CARD_ENDS_QUOTE = 'Nothing is charged now. The card subscription is updated to the new price for the rest of this period.';
201
+ // A card plan whose renewal is not known may also end with this period, so its next invoice is named only for a plan that renews by card.
202
+ const CARD_UNKNOWN_QUOTE = 'Nothing is charged now. The card subscription is updated to the new price, and a plan that renews by card shows the prorated difference on its next invoice.';
203
+ /** Only a wallet plan that has ended comes back with renew; any other plan that is not active takes a new one (manage_agent_billing). */
204
+ function renewsBack(billing) {
205
+ return billing.plan === 'prepaid' && billing.status === 'expired';
206
+ }
207
+ /** What a price drop on a wallet plan gives back now, from Wiro's own plan change. */
208
+ function walletCreditNote(change) {
209
+ const used = change.usedCreditsUsd ?? 0;
210
+ // A credit is at most 1 cent for each credit the change takes away unused, so one that takes none away gives nothing back.
211
+ if (change.creditNowUsd === 0 && used > 0 && change.creditsNow !== null && change.creditsNow >= 0) {
212
+ return 'Nothing comes back: the change takes no credits away.';
213
+ }
214
+ if (change.creditNowUsd === null) {
215
+ return 'What comes back is worked out when the change is applied: what the time left is worth, less 1 cent for each credit already used above what the plan keeps.';
216
+ }
217
+ if (change.creditNowUsd > 0) {
218
+ return `${formatUsd(change.creditNowUsd)} goes back to the balances that paid, and each keeps its own end date.`
219
+ + (used > 0 ? ` ${formatUsd(used)} comes off for credits already used above what the plan keeps.` : '');
220
+ }
221
+ return used > 0
222
+ ? 'Nothing comes back: the credits already used are worth more than the time left.'
223
+ : 'Nothing comes back to the wallet now.';
224
+ }
225
+ /**
226
+ * The quote's billing line. An active plan's money and credits come from the
227
+ * plan change Wiro works out on the plan as it is now (change); without it,
228
+ * the line says what happens without a number.
229
+ */
230
+ function quoteBillingNote(billing, delta, change, nextPrice, skillsOff) {
104
231
  // A card plan whose renewal is not known gets the new price without a renewal claim.
105
232
  const after = billing.cancelAtPeriodEnd === true
106
233
  ? ` ${cancelledNote(billing)}`
107
- : nextPrice === null
108
- ? ''
109
- : billing.cancelAtPeriodEnd === false
110
- ? ` The plan then renews at ${formatUsd(nextPrice)} a month.`
111
- : ` The new monthly price is ${formatUsd(nextPrice)}.`;
234
+ : billing.switchingToWallet
235
+ ? ` ${walletSwitchNote(billing, nextPrice)}`
236
+ : nextPrice === null
237
+ ? ''
238
+ : billing.cancelAtPeriodEnd === false
239
+ ? ` The plan then renews at ${formatUsd(nextPrice)} a month.`
240
+ : ` The new monthly price is ${formatUsd(nextPrice)}.`;
112
241
  if (billing.plan === 'none') {
113
242
  return 'The agent has no subscription yet, so nothing is charged now. The new monthly price applies once it is subscribed.';
114
243
  }
115
244
  if (billing.status !== 'active') {
116
- return `The subscription is ${billing.status ?? 'not active'}. Skills can only change on an active subscription, so renew it with manage_agent_billing first.`;
245
+ return renewsBack(billing)
246
+ ? `The subscription is ${billing.status ?? 'not active'}. Skills can only change on an active subscription, so renew it with manage_agent_billing first.`
247
+ : `The subscription is ${billing.status ?? 'not active'}. Skills can only change on an active subscription, so start a new plan with manage_agent_billing (subscribe) first.`;
117
248
  }
118
- if (delta === null) {
249
+ if (delta !== null && Math.abs(delta) < 0.005)
250
+ return 'The monthly price does not change, so nothing is charged.';
251
+ if (delta === null || change === null) {
119
252
  return billing.plan === 'prepaid'
120
- ? `The difference for the rest of this period is taken from, or credited to, ${walletName(billing)} when the change is applied. A credit can be spent until this period ends.${after}`
121
- : `Any prorated difference appears on the next invoice.${after}`;
253
+ ? `The difference for the rest of this period is taken from, or credited to, ${walletName(billing)} when the change is applied. A credit goes back to the balances that paid, and each keeps its own end date.${after}`
254
+ : cardEndsWithPeriod(billing)
255
+ ? `${CARD_ENDS_QUOTE}${after}`
256
+ : billing.cancelAtPeriodEnd === null
257
+ ? `${CARD_UNKNOWN_QUOTE}${after}`
258
+ : `Any prorated difference appears on the next invoice.${after}`;
122
259
  }
123
- if (Math.abs(delta) < 0.005)
124
- return 'The monthly price does not change, so nothing is charged.';
125
- if (billing.plan === 'card') {
126
- return `Nothing is charged now. The card subscription is updated and the prorated difference appears on the next invoice.${after}`;
260
+ const ends = creditsEndNote(skillsOff, change.creditsNow, change.creditsThisPeriod);
261
+ // Credits come with a rise when this period gains them. A plan priced again at today's rates can also
262
+ // lose credits, or keep them, while its price rises.
263
+ const gained = change.creditsNow !== null && change.creditsNow > 0 ? change.creditsNow : null;
264
+ const notGained = change.creditsNow !== null && change.creditsNow <= 0 ? ` ${ends}` : '';
265
+ if (!change.wallet) {
266
+ if (delta > 0) {
267
+ const added = gained !== null ? ` ${formatCredits(gained)} credits come now for the rest of this period.` : notGained;
268
+ if (cardEndsWithPeriod(billing))
269
+ return `${CARD_ENDS_QUOTE}${added}${after}`;
270
+ if (billing.cancelAtPeriodEnd === null)
271
+ return `${CARD_UNKNOWN_QUOTE}${added}${after}`;
272
+ return gained !== null
273
+ ? `Nothing is charged now. The prorated difference appears on the next invoice, and ${formatCredits(gained)} credits come now for the rest of this period.${after}`
274
+ : `Nothing is charged now. The prorated difference appears on the next invoice.${notGained}${after}`;
275
+ }
276
+ if (cardEndsWithPeriod(billing))
277
+ return `${CARD_ENDS_QUOTE} ${ends}${after}`;
278
+ if (billing.cancelAtPeriodEnd === null)
279
+ return `${CARD_UNKNOWN_QUOTE} ${ends}${after}`;
280
+ return `The prorated difference comes off the next invoice. ${ends}${after}`;
127
281
  }
128
- const days = billing.daysRemaining !== null ? ` for the ${billing.daysRemaining} days left in this period` : ' for the rest of this period';
129
282
  if (delta > 0) {
130
- return `About ${formatUsd(estimate ?? 0)} is taken from ${walletName(billing)} now${days}.${after}`;
283
+ const charge = formatUsd(change.chargeNowUsd ?? 0);
284
+ const credits = gained !== null ? `, and ${formatCredits(gained)} credits come with it` : '';
285
+ const days = daysLeftText(change.secondsLeft);
286
+ return underOneDayLeft(change.secondsLeft)
287
+ ? `${charge} is taken from ${walletName(billing)} now at the one-day minimum${credits}.${notGained}${after}`
288
+ : `${charge} is taken from ${walletName(billing)} now for the rest of this period${days ? ` (${days})` : ''}${credits}.${notGained}${after}`;
131
289
  }
132
- return `About ${formatUsd(Math.abs(estimate ?? 0))} is credited back to ${walletName(billing)} now${days}. ${CREDIT_SPEND_BY}${after}`;
290
+ return `${walletCreditNote(change)} ${ends}${after}`;
133
291
  }
134
292
  function chargeNote(billing, body) {
135
293
  const charged = toNumber(body.prepaidWalletDelta);
294
+ const used = toNumber(body.usedCreditsUsd) ?? 0;
295
+ const creditsNow = isPlainObject(body.pricing) ? toNumber(body.pricing.creditsNow) : null;
136
296
  const cancelled = billing.cancelAtPeriodEnd === true && billing.status === 'active' ? ` ${cancelledNote(billing)}` : '';
137
297
  if (charged !== null && charged > 0) {
138
- return { charged, note: `${formatUsd(charged)} was taken from ${walletName(billing)} for the rest of this period.${cancelled}` };
298
+ const credits = creditsNow !== null && creditsNow > 0 ? `, and ${formatCredits(creditsNow)} credits came with it` : '';
299
+ return { charged, note: `${formatUsd(charged)} was taken from ${walletName(billing)} for the rest of this period${credits}.${cancelled}` };
139
300
  }
140
301
  if (charged !== null && charged < 0) {
141
- return { charged, note: `${formatUsd(Math.abs(charged))} was credited to ${walletName(billing)}. ${CREDIT_SPEND_BY}${cancelled}` };
302
+ const terms = creditTerms(body.prepaidCreditParts);
303
+ const usedNote = used > 0 ? ` ${formatUsd(used)} came off for credits already used above what the plan keeps.` : '';
304
+ return { charged, note: `${formatUsd(Math.abs(charged))} was credited to ${walletName(billing)}.${terms ? ` ${terms}` : ''}${usedNote}${cancelled}` };
305
+ }
306
+ // A change that took no credits away had nothing to credit, and nothing failed.
307
+ if (billing.plan === 'prepaid' && used > 0 && creditsNow !== null && creditsNow >= 0) {
308
+ return { charged: null, note: `Nothing came back: the change took no credits away.${cancelled}` };
309
+ }
310
+ // The credits already used came to more than the time left was worth: no credit, and nothing failed.
311
+ // A part Wiro could not add (prepaidCreditFailedUsd) was due all the same: its warning follows, and the
312
+ // credits used took only what Wiro names.
313
+ if (billing.plan === 'prepaid' && used > 0) {
314
+ return (toNumber(body.prepaidCreditFailedUsd) ?? 0) > 0
315
+ ? { charged: null, note: `Nothing came back now. ${formatUsd(used)} came off for credits already used above what the plan keeps.${cancelled}` }
316
+ : { charged: null, note: `Nothing came back: the credits already used were worth more than the time left.${cancelled}` };
142
317
  }
143
318
  if (body.stripeProrationApplied === true) {
144
- return { charged: null, note: `The card subscription was updated; any prorated difference appears on the next invoice.${cancelled}` };
319
+ const moved = isPlainObject(body.pricing) ? toNumber(body.pricing.deltaUsd) : null;
320
+ if (moved !== null && Math.abs(moved) < 0.005) {
321
+ return { charged: null, note: `The monthly price did not change, so nothing was charged.${cancelled}` };
322
+ }
323
+ const added = moved !== null && moved > 0 && creditsNow !== null && creditsNow > 0
324
+ ? ` ${formatCredits(creditsNow)} credits came now for the rest of this period.`
325
+ : '';
326
+ if (cardEndsWithPeriod(billing)) {
327
+ const price = isPlainObject(body.pricing) ? toNumber(body.pricing.newPriceUsd) : null;
328
+ const switching = billing.switchingToWallet ? ` ${walletSwitchNote(billing, price)}` : '';
329
+ return { charged: null, note: `Nothing was charged now. The card subscription was updated to the new price for the rest of this period.${added}${cancelled}${switching}` };
330
+ }
331
+ if (billing.cancelAtPeriodEnd === null) {
332
+ return { charged: null, note: `Nothing was charged now. The card subscription was updated to the new price, and a plan that renews by card shows the prorated difference on its next invoice.${added}` };
333
+ }
334
+ return { charged: null, note: `The card subscription was updated; any prorated difference appears on the next invoice.${added}${cancelled}` };
145
335
  }
146
336
  if (billing.plan === 'none') {
147
337
  return { charged: null, note: 'Nothing was charged: the agent has no subscription yet.' };
@@ -156,6 +346,21 @@ function appliedBillingNote(billing, body) {
156
346
  .filter((warning) => Boolean(warning));
157
347
  return warnings.length > 0 ? { ...result, note: [result.note, ...warnings].join(' ') } : result;
158
348
  }
349
+ /** The sentence for a change whose skills need a Wiro project. */
350
+ function bindProjectNote(needed) {
351
+ return needed.some(entry => entry.credentials.includes(WIRO_PROJECT_CREDENTIAL))
352
+ ? ' bind_agent_project connects a Wiro project, with no key to paste.'
353
+ : '';
354
+ }
355
+ /** How to give the new skills what they still need. A Wiro project comes first, since it takes no key. */
356
+ function stillNeededSteps(needed) {
357
+ const keys = needed.flatMap(entry => entry.credentials);
358
+ const fields = 'set_agent_credentials or connect_agent_oauth.';
359
+ if (!keys.includes(WIRO_PROJECT_CREDENTIAL))
360
+ return `Set them with ${fields}`;
361
+ const others = keys.some(key => key !== WIRO_PROJECT_CREDENTIAL) ? ` Set the others with ${fields}` : '';
362
+ return `Connect the Wiro project with bind_agent_project, with no key to paste.${others}`;
363
+ }
159
364
  function listLine(label, names) {
160
365
  return names.length > 0 ? `**${label}:** ${names.join(', ')}` : null;
161
366
  }
@@ -180,21 +385,26 @@ export function registerApplyAgentSkills(server, client) {
180
385
  + 'On a team agent only a team admin or the person who deployed the agent can change its skills; other '
181
386
  + 'members are refused and nothing is charged. '
182
387
  + 'Steps: 1) call without confirm. The tool checks the names, adds the skills they '
183
- + 'depend on, and returns the current and new monthly price, an estimate of what is '
184
- + 'charged now and an idempotency_key. Nothing changes yet. 2) Show the price to the '
388
+ + 'depend on, and returns the current and new monthly price, what Wiro charges or credits now, '
389
+ + 'the credits this period gains or loses, and an idempotency_key. Nothing changes yet. 2) Show the price to the '
185
390
  + 'user. 3) After they approve, call again with confirm: true and that idempotency_key. '
186
- + 'The key is tied to the quoted prices: if the price changed since the quote, nothing '
187
- + 'is applied and a new quote with a new key comes back. Reusing the key after a timeout '
391
+ + 'The key is tied to the quoted prices and, on a prepaid plan, the billing period: if '
392
+ + 'either changed since the quote, nothing is applied and a new quote with a new key comes back. Reusing the key after a timeout '
188
393
  + 'is safe, the change is applied once. Billing: on a prepaid plan the monthly difference '
189
- + 'for the days left in the period is taken from the wallet at once, or credited to it '
190
- + 'when the price drops, and that credit can be spent until the period ends; on a card '
191
- + 'plan it is added to the next invoice. A running agent '
192
- + 'restarts once to load the skills. Newly enabled skills may still need credentials '
193
- + '(set_agent_credentials, connect_agent_oauth) and do not create scheduled tasks '
394
+ + 'for the time left in the period is taken from the wallet at once, and the added credits come '
395
+ + 'for that same time; every renewal gives all of them. When the price drops, what the time left '
396
+ + 'is worth goes back to the balances that paid, less 1 cent for each credit already used above '
397
+ + 'what the plan keeps, and each balance keeps its own end date. A skill that comes off loses its '
398
+ + 'credits for the time left; the ones the time already passed paid for stay until the renewal. '
399
+ + 'On a card plan nothing is charged at once: the card subscription is updated to the new price, '
400
+ + 'a plan that renews by card shows the difference on its next invoice, and the added credits come '
401
+ + 'for the time left. A running agent restarts once when a skill turns on or off; a change of the '
402
+ + 'tier alone leaves it running. Newly enabled skills may still need credentials '
403
+ + '(set_agent_credentials, connect_agent_oauth, bind_agent_project for a Wiro project) and do not create scheduled tasks '
194
404
  + '(manage_agent_custom_skill does). Find skill names with list_agent_skills and the '
195
405
  + 'agent\'s current skills with get_my_agent. The agents playbook (get_playbook) has the '
196
406
  + 'full setup walkthrough.',
197
- inputSchema: {
407
+ inputSchema: strictInput({
198
408
  agent_guid: z.string().min(1).describe('The deployed agent\'s guid, from list_my_agents.'),
199
409
  skills: z.record(z.string(), z.boolean())
200
410
  .describe('Skills to change, as name: true to turn on or name: false to turn off, for example {"int-instagram-post": true}. Registry names only (list_agent_skills). Custom skills and scheduled tasks (cs-...) are managed with manage_agent_custom_skill; channels such as Telegram turn on through set_agent_credentials.'),
@@ -205,12 +415,12 @@ export function registerApplyAgentSkills(server, client) {
205
415
  confirm: z.boolean().default(false)
206
416
  .describe('false (default) only returns the quote. true applies the change and charges it; it needs the idempotency_key from the quote.'),
207
417
  idempotency_key: z.string().min(8).max(128).optional()
208
- .describe('With confirm: true, the key from the quote the user approved. It ties the approval to the quoted prices, and repeating the call with it after a timeout never applies the change twice. Omit it to get a quote.'),
209
- },
418
+ .describe('With confirm: true, the key from the quote the user approved. It ties the approval to the quoted prices and, on a prepaid plan, the billing period, and repeating the call with it after a timeout never applies the change twice. Omit it to get a quote.'),
419
+ }),
210
420
  outputSchema,
211
421
  annotations: {
212
422
  readOnlyHint: false,
213
- destructiveHint: false,
423
+ destructiveHint: true,
214
424
  idempotentHint: true,
215
425
  openWorldHint: true,
216
426
  },
@@ -257,7 +467,9 @@ export function registerApplyAgentSkills(server, client) {
257
467
  alreadySet: diff.alreadySet.filter(name => !plan.addedDependencies.includes(name)),
258
468
  };
259
469
  const credentialsNeeded = credentialsNeededFor(diff.enable, registry, row);
260
- const nothingToDo = Object.keys(diff.changes).length === 0 && !tierChange;
470
+ // A running agent restarts only for a skill that turns on or off.
471
+ const skillsMove = Object.keys(diff.changes).length > 0;
472
+ const nothingToDo = !skillsMove && !tierChange;
261
473
  const base = {
262
474
  agentGuid: agent_guid,
263
475
  changes,
@@ -281,7 +493,8 @@ export function registerApplyAgentSkills(server, client) {
281
493
  };
282
494
  }
283
495
  const currentPrice = toNumber(row.monthlypriceusd);
284
- const currentCredits = toNumber(row.monthlycredits);
496
+ // The plan's monthly figure is what each renewal grants. This period's can differ after a change inside it.
497
+ const currentCredits = toNumber(row.renewalcredits) ?? toNumber(row.monthlycredits);
285
498
  const key = idempotency_key?.trim() ?? '';
286
499
  // Every team member can read a team agent and price a change, but only a
287
500
  // team admin or the person who deployed it can apply one. The credit
@@ -314,19 +527,19 @@ export function registerApplyAgentSkills(server, client) {
314
527
  if (newPrice === null) {
315
528
  throw new Error('Wiro did not return the new monthly price of this change, so it cannot be quoted. Try again later.');
316
529
  }
317
- const priceTag = skillPriceTag(currentPrice, newPrice);
530
+ const priceTag = skillPriceTag(currentPrice, newPrice, billing.plan === 'prepaid' ? billing.periodEnd : null);
318
531
  const approved = confirm && givenKey !== null && givenKey.priceTag === priceTag;
319
532
  if (!approved) {
320
533
  const newCredits = toNumber(preview.totalMonthlyCredits);
321
534
  const delta = difference(newPrice, currentPrice);
322
- const estimate = billing.plan === 'prepaid' && billing.status === 'active' && delta !== null
323
- ? estimateProratedUsd(delta, billing.periodEnd)
324
- : null;
325
- const note = quoteBillingNote(billing, delta, estimate, newPrice);
535
+ // What Wiro works out for this change on the plan as it is now. Only an active plan has one.
536
+ const change = billing.plan !== 'none' && billing.status === 'active' ? readPlanChange(preview.planChange) : null;
537
+ const note = quoteBillingNote(billing, delta, change, newPrice, changes.disable.length);
538
+ const walletDrop = change !== null && change.wallet && delta !== null && delta < 0;
326
539
  const quoteKey = givenKey?.priceTag === priceTag ? key : newSkillChangeKey(digest, priceTag);
327
540
  const priceChanged = confirm && givenKey !== null;
328
541
  const blocked = billing.plan !== 'none' && billing.status !== 'active';
329
- const restart = restartNote(status, 'skill change');
542
+ const restart = skillsMove ? restartNote(status, 'skill change') : TIER_ONLY_RESTART;
330
543
  const priceNote = todayPriceNote(row, billing, currentPrice, currentCredits, tierChange);
331
544
  const pricing = {
332
545
  currentMonthlyUsd: currentPrice,
@@ -335,12 +548,13 @@ export function registerApplyAgentSkills(server, client) {
335
548
  currentMonthlyCredits: currentCredits,
336
549
  newMonthlyCredits: newCredits,
337
550
  deltaMonthlyCredits: difference(newCredits, currentCredits),
551
+ ...periodFields(change),
338
552
  ...(priceNote ? { note: priceNote } : {}),
339
553
  };
340
554
  const nextAction = blocked
341
555
  ? {
342
556
  tool: 'manage_agent_billing',
343
- arguments: { agent_guid, action: 'renew' },
557
+ arguments: { agent_guid, action: renewsBack(billing) ? 'renew' : 'subscribe' },
344
558
  reason: 'The subscription must be active before skills can change.',
345
559
  }
346
560
  : {
@@ -360,7 +574,7 @@ export function registerApplyAgentSkills(server, client) {
360
574
  const lead = !confirm
361
575
  ? null
362
576
  : priceChanged
363
- ? 'The price changed since the quote, so nothing was applied or charged. This is the new quote with a new idempotency_key; show it to the user and ask again.'
577
+ ? 'The price or the billing period changed since the quote, so nothing was applied or charged. This is the new quote with a new idempotency_key; show it to the user and ask again.'
364
578
  : 'Nothing was applied: confirming needs the idempotency_key of a quote, which ties the approval to the price. This is the quote; show it to the user first.';
365
579
  const lines = [
366
580
  '## Skill change quote',
@@ -374,15 +588,20 @@ export function registerApplyAgentSkills(server, client) {
374
588
  tierChange ? `**Plan:** ${currentTier ?? 'unknown'} to ${tierChange}` : null,
375
589
  `**Monthly price:** ${currentPrice !== null ? formatUsd(currentPrice) : 'unknown'} now, ${formatUsd(newPrice)} after the change${delta !== null ? ` (${delta >= 0 ? '+' : ''}${formatUsd(delta)})` : ''}`,
376
590
  priceNote ? `**Price update:** ${priceNote}` : null,
377
- newCredits !== null ? `**Monthly credits:** ${currentCredits ?? 'unknown'} now, ${newCredits} after the change` : null,
591
+ change?.creditsFromRenewal !== null && change?.creditsFromRenewal !== undefined
592
+ ? `**Monthly credits:** ${currentCredits !== null ? formatCredits(currentCredits) : 'unknown'} now, ${formatCredits(change.creditsFromRenewal)} ${renewalWhen(billing)}`
593
+ : newCredits !== null ? `**Monthly credits:** ${currentCredits !== null ? formatCredits(currentCredits) : 'unknown'} now, ${formatCredits(newCredits)} ${creditsWhen(billing)}` : null,
594
+ change && change.creditsNow !== null && change.creditsThisPeriod !== null
595
+ ? `**This period:** ${signedCredits(change.creditsNow)} credits now, ${formatCredits(change.creditsThisPeriod)} in all until ${unixToIso(change.periodEnd) ?? 'the period ends'}`
596
+ : null,
378
597
  `**Billing:** ${note}`,
379
598
  `**Restart:** ${restart}`,
380
599
  credentialsNeeded.length > 0
381
- ? `**Credentials the new skills need:** ${credentialsNeeded.map(entry => `${entry.skill} (${entry.credentials.join(', ')})`).join('; ')}`
600
+ ? `**Credentials the new skills need:** ${credentialsNeeded.map(entry => `${entry.skill} (${entry.credentials.join(', ')})`).join('; ')}.${bindProjectNote(credentialsNeeded)}`
382
601
  : null,
383
602
  '',
384
603
  blocked
385
- ? 'Renew the subscription with manage_agent_billing before applying this change.'
604
+ ? `${renewsBack(billing) ? 'Renew the subscription' : 'Start a new plan'} with manage_agent_billing before applying this change.`
386
605
  : `Nothing has changed yet. After the user approves, call apply_agent_skills again with confirm: true and idempotency_key \`${quoteKey}\`.`,
387
606
  ].filter((line) => line !== null);
388
607
  return {
@@ -393,7 +612,12 @@ export function registerApplyAgentSkills(server, client) {
393
612
  idempotencyKey: quoteKey,
394
613
  ...(priceChanged ? { priceChanged: true } : {}),
395
614
  pricing,
396
- billing: { ...billingBase, estimatedChargeNowUsd: estimate, note },
615
+ billing: {
616
+ ...billingBase,
617
+ estimatedChargeNowUsd: change ? change.chargeNowUsd : null,
618
+ ...(walletDrop ? { creditNowUsd: change.creditNowUsd, usedCreditsUsd: change.usedCreditsUsd } : {}),
619
+ note,
620
+ },
397
621
  ...(Array.isArray(preview.enabledSkills) ? { enabledSkills: preview.enabledSkills.map(String) } : {}),
398
622
  restart,
399
623
  nextAction,
@@ -416,8 +640,15 @@ export function registerApplyAgentSkills(server, client) {
416
640
  throw error;
417
641
  if (!outcomeUnknown(error)) {
418
642
  const failure = describeSkillsApplyError(error, billing.plan);
643
+ // Code 110 is the whole outcome: the next step reads the agent, and the call is not repeated.
644
+ const failedChange = failure.code === 110;
645
+ const nextAction = failedChange
646
+ ? { tool: 'get_my_agent', arguments: { agent_guid }, reason: 'Check the agent\'s skills and monthly price. Do not repeat the change.' }
647
+ : failure.code === 107
648
+ ? { tool: 'apply_agent_skills', arguments: { agent_guid, skills, ...(tier ? { tier } : {}), add_dependencies }, reason: 'Get a new quote for this change, with a new key.' }
649
+ : undefined;
419
650
  const text = [
420
- '## Skill change not applied',
651
+ failedChange ? '## Skill change failed' : '## Skill change not applied',
421
652
  '',
422
653
  failure.message,
423
654
  failure.guidance ? `\n${failure.guidance}` : '',
@@ -430,7 +661,7 @@ export function registerApplyAgentSkills(server, client) {
430
661
  status: 'refused',
431
662
  idempotencyKey: key,
432
663
  billing: { ...billingBase, note: failure.guidance || failure.message },
433
- restart: UNCHANGED_RESTART,
664
+ restart: failedChange ? 'The agent does not restart for this change.' : UNCHANGED_RESTART,
434
665
  error: {
435
666
  code: failure.code,
436
667
  message: failure.message,
@@ -439,6 +670,7 @@ export function registerApplyAgentSkills(server, client) {
439
670
  ...(failure.requiredUsd !== undefined ? { requiredUsd: failure.requiredUsd } : {}),
440
671
  ...(failure.availableUsd !== undefined ? { availableUsd: failure.availableUsd } : {}),
441
672
  },
673
+ ...(nextAction ? { nextAction } : {}),
442
674
  },
443
675
  isError: true,
444
676
  };
@@ -446,7 +678,9 @@ export function registerApplyAgentSkills(server, client) {
446
678
  const message = errorText(error);
447
679
  if (sentEmpty) {
448
680
  const note = 'The agent\'s skills already match the request, so the change is in place. Do not repeat it. '
449
- + 'What the earlier call with this idempotency_key charged is not shown here; get_agent_activity with kind usage shows it.';
681
+ + (billing.plan === 'prepaid'
682
+ ? `What the earlier call with this idempotency_key took from, or credited to, ${walletName(billing)} is not shown here; get_account with recent_transactions shows it.`
683
+ : 'Any price difference follows the plan\'s usual billing.');
450
684
  return {
451
685
  content: [{ type: 'text', text: `## No change needed\n\n${note}` }],
452
686
  structuredContent: {
@@ -462,7 +696,7 @@ export function registerApplyAgentSkills(server, client) {
462
696
  // before anything is reported.
463
697
  const holds = await changeHolds(client, agent_guid, plan.requested, tierChange, extra.signal);
464
698
  if (holds === true) {
465
- const note = `The agent's skills now match the request, so the change is in place. Do not repeat it. ${billing.plan === 'prepaid' && billing.status === 'active' ? `The difference for the rest of this period was probably taken from, or credited to, ${walletName(billing)}, and a credit can be spent until this period ends; get_agent_activity with kind usage and get_account show it.` : 'Any price difference follows the plan\'s usual billing.'}`;
699
+ const note = `The agent's skills now match the request, so the change is in place. Do not repeat it. ${billing.plan === 'prepaid' && billing.status === 'active' ? `The difference for the rest of this period was probably taken from, or credited to, ${walletName(billing)}; a credit goes back to the balances that paid. get_account with recent_transactions shows the amount, and get_agent_activity with kind usage the credits it moved.` : 'Any price difference follows the plan\'s usual billing.'}`;
466
700
  return {
467
701
  content: [{ type: 'text', text: [
468
702
  '## Skills updated',
@@ -471,14 +705,14 @@ export function registerApplyAgentSkills(server, client) {
471
705
  listLine('Turned on', changes.enable),
472
706
  listLine('Turned off', changes.disable),
473
707
  `**Billing:** ${note}`,
474
- `**Restart:** ${restartNote(status, 'skill change')}`,
708
+ `**Restart:** ${skillsMove ? restartNote(status, 'skill change') : TIER_ONLY_RESTART}`,
475
709
  ].filter((line) => line !== null).join('\n') }],
476
710
  structuredContent: {
477
711
  ...base,
478
712
  status: 'applied',
479
713
  idempotencyKey: key,
480
714
  billing: { ...billingBase, chargedNowUsd: null, note },
481
- restart: restartNote(status, 'skill change'),
715
+ restart: skillsMove ? restartNote(status, 'skill change') : TIER_ONLY_RESTART,
482
716
  },
483
717
  };
484
718
  }
@@ -493,7 +727,7 @@ export function registerApplyAgentSkills(server, client) {
493
727
  status: 'refused',
494
728
  idempotencyKey: key,
495
729
  billing: { ...billingBase, note },
496
- restart: unverified ? 'The agent restarts if the change was saved.' : UNCHANGED_RESTART,
730
+ restart: !unverified ? UNCHANGED_RESTART : skillsMove ? 'The agent restarts if the change was saved.' : TIER_ONLY_RESTART,
497
731
  error: { code: null, message },
498
732
  },
499
733
  isError: true,
@@ -527,6 +761,13 @@ export function registerApplyAgentSkills(server, client) {
527
761
  // result of the earlier call with this key, not a new charge.
528
762
  const replayed = sentEmpty && !applied.noOp;
529
763
  const serverPricing = applied.pricing ?? {};
764
+ // An agent with no plan has no period and no renewal yet, as its quote says.
765
+ const appliedChange = billing.plan === 'none' ? null : readPlanChange({
766
+ creditsNow: serverPricing.creditsNow,
767
+ creditsThisPeriod: serverPricing.creditsThisPeriod,
768
+ creditsFromRenewal: serverPricing.creditsFromRenewal,
769
+ periodEnd: serverPricing.periodEnd,
770
+ });
530
771
  const pricing = applied.noOp
531
772
  ? undefined
532
773
  : {
@@ -536,30 +777,36 @@ export function registerApplyAgentSkills(server, client) {
536
777
  currentMonthlyCredits: toNumber(serverPricing.previousMonthlyCredits),
537
778
  newMonthlyCredits: toNumber(serverPricing.newMonthlyCredits),
538
779
  deltaMonthlyCredits: toNumber(serverPricing.deltaCredits),
780
+ ...periodFields(appliedChange),
539
781
  };
540
782
  const charge = appliedBillingNote(billing, applied);
783
+ const usedCreditsUsd = toNumber(applied.usedCreditsUsd);
541
784
  const billingNote = replayed
542
785
  ? `This change was already applied by an earlier call with this idempotency_key, and nothing new was charged now. That call's billing: ${charge.note}`
543
786
  : charge.note;
787
+ // Wiro says whether this change restarts a running agent; an answer without it goes by the skills that move.
788
+ const restarts = typeof applied.restartTriggered === 'boolean' ? applied.restartTriggered : skillsMove;
544
789
  const restart = applied.noOp || replayed
545
790
  ? UNCHANGED_RESTART
546
- : restartNote(status, 'skill change');
791
+ : restarts ? restartNote(status, 'skill change') : TIER_ONLY_RESTART;
547
792
  const firstCredential = credentialsNeeded[0]?.credentials[0];
548
793
  const nextAction = applied.noOp || replayed
549
794
  ? undefined
550
- : firstCredential
551
- ? {
552
- tool: 'set_agent_credentials',
553
- arguments: { agent_guid, credential: firstCredential },
554
- reason: `See what ${firstCredential} needs before asking the user for it.`,
555
- }
556
- : running
795
+ : firstCredential === WIRO_PROJECT_CREDENTIAL
796
+ ? bindProjectStep(agent_guid, 'Connect the Wiro project the new skills need.')
797
+ : firstCredential
557
798
  ? {
558
- tool: 'get_my_agent',
559
- arguments: { agent_guid, wait_seconds: DEFAULT_WAIT_SECONDS },
560
- reason: 'Check that the agent is running again with the new skills.',
799
+ tool: 'set_agent_credentials',
800
+ arguments: { agent_guid, credential: firstCredential },
801
+ reason: `See what ${firstCredential} needs before asking the user for it.`,
561
802
  }
562
- : undefined;
803
+ : running && restarts
804
+ ? {
805
+ tool: 'get_my_agent',
806
+ arguments: { agent_guid, wait_seconds: DEFAULT_WAIT_SECONDS },
807
+ reason: 'Check that the agent is running again with the new skills.',
808
+ }
809
+ : undefined;
563
810
  const lines = [
564
811
  applied.noOp ? '## No change needed' : replayed ? '## Already applied' : '## Skills updated',
565
812
  '',
@@ -571,11 +818,14 @@ export function registerApplyAgentSkills(server, client) {
571
818
  pricing?.newMonthlyUsd !== undefined && pricing.newMonthlyUsd !== null
572
819
  ? `**Monthly price:** ${formatUsd(pricing.newMonthlyUsd)}${pricing.currentMonthlyUsd !== null ? ` (was ${formatUsd(pricing.currentMonthlyUsd)})` : ''}`
573
820
  : null,
821
+ pricing && appliedChange?.creditsNow !== null && appliedChange?.creditsNow !== undefined && appliedChange.creditsThisPeriod !== null
822
+ ? `**This period:** ${signedCredits(appliedChange.creditsNow)} credits, ${formatCredits(appliedChange.creditsThisPeriod)} in all until ${unixToIso(appliedChange.periodEnd) ?? 'the period ends'}${appliedChange.creditsFromRenewal !== null ? `; ${renewalGives(billing, appliedChange.creditsFromRenewal)}` : ''}`
823
+ : null,
574
824
  `**Billing:** ${billingNote}`,
575
825
  `**Restart:** ${restart}`,
576
826
  holds === null ? 'The agent could not be read afterwards to confirm the new skills; get_my_agent shows them.' : null,
577
827
  credentialsNeeded.length > 0
578
- ? `**Still needed:** credentials for ${credentialsNeeded.map(entry => `${entry.skill} (${entry.credentials.join(', ')})`).join('; ')}. Set them with set_agent_credentials or connect_agent_oauth.`
828
+ ? `**Still needed:** credentials for ${credentialsNeeded.map(entry => `${entry.skill} (${entry.credentials.join(', ')})`).join('; ')}. ${stillNeededSteps(credentialsNeeded)}`
579
829
  : null,
580
830
  ].filter((line) => line !== null);
581
831
  return {
@@ -585,7 +835,13 @@ export function registerApplyAgentSkills(server, client) {
585
835
  status: applied.noOp ? 'no_change' : 'applied',
586
836
  idempotencyKey: key,
587
837
  ...(pricing ? { pricing } : {}),
588
- billing: { ...billingBase, chargedNowUsd: charge.charged, note: billingNote },
838
+ // The saved answer of an earlier call: its charge is in the note, and this call charged nothing.
839
+ billing: {
840
+ ...billingBase,
841
+ chargedNowUsd: replayed ? 0 : charge.charged,
842
+ ...(usedCreditsUsd !== null && usedCreditsUsd > 0 ? { usedCreditsUsd } : {}),
843
+ note: billingNote,
844
+ },
589
845
  ...(Array.isArray(serverPricing.enabledSkills) ? { enabledSkills: serverPricing.enabledSkills.map(String) } : {}),
590
846
  restart,
591
847
  ...(nextAction ? { nextAction } : {}),