@wiro-ai/wiro-mcp 1.4.1 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +46 -35
  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 +458 -109
  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 +405 -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 +161 -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 +239 -46
  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 +13 -17
  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 +13 -8
  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 +11 -8
  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 +119 -15
  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 +55 -61
  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 +59 -20
  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 +59 -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 +172 -9
  166. package/dist/utils/agent-config.d.ts.map +1 -1
  167. package/dist/utils/agent-config.js +416 -60
  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 +197 -29
  207. package/dist/utils/user-agent.d.ts.map +1 -1
  208. package/dist/utils/user-agent.js +314 -48
  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, credentialLabel, credentialTitles, credentialsNeededFor, customSkillEntries, daysLeftText, describeSkillsApplyError, diffSkills, enabledSkillNames, formatUsd, isAgentRunning, loadSkillRegistry, newSkillChangeKey, parseSkillChangeKey, pausedSkillTitles, planBilling, planSkillChanges, readPlanChange, readUserAgent, registryTitles, underOneDayLeft, restartNote, skillChangeDigest, skillPriceTag, workspaceHint, } from '../utils/agent-config.js';
4
+ import { asTier, bindProjectStep, formatCredits, outcomeUnknown, skillLabel, skillOffTasksNote, taskLabel, taskNamedByWiro, toStringArray, 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
  }),
@@ -43,6 +58,15 @@ const outputSchema = {
43
58
  skill: z.string(),
44
59
  credentials: z.array(z.string()),
45
60
  })),
61
+ scheduledTasks: z.object({
62
+ stop: z.array(z.string())
63
+ .describe('Scheduled tasks this change stops: they run now, or would once an agent that is not running starts. They keep their text, schedule and on/off state, and get_my_agent then shows what each waits for.'),
64
+ resume: z.array(z.string())
65
+ .describe('Scheduled tasks that do not run now and start running once this change is applied, and the agent is running.'),
66
+ }).nullable().optional()
67
+ .describe('Quote: the scheduled tasks this change stops or starts, as Wiro works it out; null when Wiro could not tell.'),
68
+ pausedTasks: z.array(z.string()).optional()
69
+ .describe('After an applied change: the scheduled tasks it paused, which wait for skills that are off. get_my_agent names those skills.'),
46
70
  agentStatus: z.string(),
47
71
  restart: z.string(),
48
72
  error: z.object({
@@ -56,9 +80,64 @@ const outputSchema = {
56
80
  nextAction: nextActionSchema.optional(),
57
81
  };
58
82
  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.';
83
+ // The tier and the price live with Wiro, and the agent reads neither: only a skill that turns on or off restarts it.
84
+ const TIER_ONLY_RESTART = 'No skill turns on or off, so a change of the tier alone does not restart the agent.';
85
+ function roundCents(usd) {
86
+ return Math.round(usd * 100) / 100;
87
+ }
88
+ /**
89
+ * How long an applied credit lasts, from the parts Wiro says it split into. Each
90
+ * part went back to the balance that paid it and keeps that balance's end date,
91
+ * except the part whose source has ended since (each ...UntilPeriodEndUsd): that
92
+ * part lasts until the plan's current period ends. The two amounts add up to the
93
+ * credit, and parts that come to nothing say nothing.
94
+ */
95
+ function creditTerms(parts) {
96
+ if (!isPlainObject(parts))
97
+ return '';
98
+ const usd = (key) => toNumber(parts[key]) ?? 0;
99
+ const untilEnd = roundCents(usd('couponUntilPeriodEndUsd') + usd('storeUntilPeriodEndUsd') + usd('depositUntilPeriodEndUsd'));
100
+ const keepsEnd = roundCents(roundCents(usd('couponUsd') + usd('storeUsd') + usd('depositUsd')) - untilEnd);
101
+ if (keepsEnd > 0 && untilEnd > 0) {
102
+ 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.`;
103
+ }
104
+ if (keepsEnd > 0)
105
+ return 'It went back to the balances that paid and keeps their end dates.';
106
+ return untilEnd > 0 ? 'It lasts until this period ends, because the credit that paid it has ended.' : '';
107
+ }
108
+ /** A count of credits with its sign, such as +97 or -690. */
109
+ function signedCredits(credits) {
110
+ return `${credits > 0 ? '+' : ''}${formatCredits(credits)}`;
111
+ }
112
+ /** The fields of Wiro's plan change that the pricing output carries. */
113
+ function periodFields(change) {
114
+ if (!change)
115
+ return {};
116
+ const periodEnd = unixToIso(change.periodEnd);
117
+ return {
118
+ ...(change.creditsNow !== null ? { creditsNow: change.creditsNow } : {}),
119
+ ...(change.creditsThisPeriod !== null ? { creditsThisPeriod: change.creditsThisPeriod } : {}),
120
+ ...(change.creditsFromRenewal !== null ? { creditsFromRenewal: change.creditsFromRenewal } : {}),
121
+ ...(periodEnd ? { periodEnd } : {}),
122
+ };
123
+ }
124
+ /**
125
+ * What a change does to this period's credits when none come with it: those it takes away end for the
126
+ * time left, and the period keeps the rest. A plan priced again at today's rates can also keep its
127
+ * credits, or gain some, while its price drops.
128
+ */
129
+ function creditsEndNote(skillsOff, creditsNow, creditsThisPeriod) {
130
+ if (creditsNow !== null && creditsNow > 0) {
131
+ return `This period gains ${formatCredits(creditsNow)} credits for the time left${creditsThisPeriod !== null ? `, ${formatCredits(creditsThisPeriod)} in all` : ''}.`;
132
+ }
133
+ if (creditsNow === 0) {
134
+ return creditsThisPeriod !== null ? `This period keeps its ${formatCredits(creditsThisPeriod)} credits.` : 'This period keeps its credits.';
135
+ }
136
+ const keeps = creditsThisPeriod !== null ? `, and this period keeps ${formatCredits(creditsThisPeriod)}` : '';
137
+ if (skillsOff === 0)
138
+ return `The credits the change takes away end now for the time left${keeps}.`;
139
+ return `${skillsOff === 1 ? 'Its' : 'Their'} credits for the time left end now${keeps}.`;
140
+ }
62
141
  function difference(next, current) {
63
142
  return next === null || current === null ? null : Math.round((next - current) * 100) / 100;
64
143
  }
@@ -66,9 +145,10 @@ function difference(next, current) {
66
145
  * A skill change prices the plan at today's prices for all its skills, so a
67
146
  * plan that started at other prices also changes for the skills it already
68
147
  * 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.
148
+ * and credits for those skills. They are the base of the new price, not a price
149
+ * the plan reaches. An agent without an active plan cannot take the change yet,
150
+ * and a move to Pro prices the whole plan at today's Pro prices, which the quote
151
+ * already shows.
72
152
  */
73
153
  function todayPriceNote(row, billing, currentPrice, currentCredits, tierChange) {
74
154
  if (tierChange)
@@ -76,12 +156,12 @@ function todayPriceNote(row, billing, currentPrice, currentCredits, tierChange)
76
156
  const tier = asTier(row.tier);
77
157
  const tiers = toTierPricing(isPlainObject(row.agent) ? row.agent.tiers : undefined);
78
158
  const today = tier && tiers ? tiers[tier] : null;
79
- if (billing.plan === 'none' || currentPrice === null || today === null || !difference(today.priceUsd, currentPrice))
159
+ if (billing.status !== 'active' || currentPrice === null || today === null || !difference(today.priceUsd, currentPrice))
80
160
  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}.`;
161
+ const versus = currentCredits !== null && today.monthlyCredits !== currentCredits
162
+ ? ` with ${formatCredits(today.monthlyCredits)} monthly credits, not its current ${formatUsd(currentPrice)} and ${formatCredits(currentCredits)}`
163
+ : `, not its current ${formatUsd(currentPrice)}`;
164
+ 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
165
  }
86
166
  /**
87
167
  * The wallet of the agent's workspace, which Wiro charges or credits for a
@@ -92,56 +172,175 @@ function todayPriceNote(row, billing, currentPrice, currentCredits, tierChange)
92
172
  function walletName(billing) {
93
173
  return billing.wallet === 'team' ? 'the team wallet' : 'your personal wallet';
94
174
  }
95
- /** For a cancelled plan, a skill change only covers the days left: nothing renews at the new price. */
175
+ /** 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. */
176
+ function creditsWhen(billing) {
177
+ if (billing.plan === 'none')
178
+ return 'a month once it is subscribed';
179
+ return billing.status === 'active' && billing.cancelAtPeriodEnd === true
180
+ ? 'at each renewal after the change, if the plan is kept'
181
+ : 'at each renewal after the change';
182
+ }
183
+ /** 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. */
184
+ function renewalWhen(billing) {
185
+ if (billing.cancelAtPeriodEnd === true)
186
+ return 'from the next renewal, if the plan is kept';
187
+ return billing.cancelAtPeriodEnd === null ? 'from the next renewal, if the plan renews' : 'from the next renewal';
188
+ }
189
+ /** What renewals give after an applied change: a plan that may not renew names it as a condition, as its quote did. */
190
+ function renewalGives(billing, credits) {
191
+ return billing.cancelAtPeriodEnd === false ? `every renewal gives ${formatCredits(credits)}` : `${formatCredits(credits)} ${renewalWhen(billing)}`;
192
+ }
193
+ /** For a cancelled plan, a skill change only covers the time left: nothing renews at the new price. */
96
194
  function cancelledNote(billing) {
97
195
  const end = billing.periodEnd !== null ? unixToIso(billing.periodEnd) : undefined;
98
196
  if (billing.plan === 'card') {
99
197
  return `The plan is set to end${end ? ` after ${end}` : ' with this period'} and does not renew.`;
100
198
  }
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.`;
199
+ 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.`;
102
200
  }
103
- function quoteBillingNote(billing, delta, estimate, nextPrice) {
201
+ /** A card subscription that ends with this period: the plan is set to end, or it moves to wallet billing. */
202
+ function cardEndsWithPeriod(billing) {
203
+ return billing.plan === 'card' && (billing.cancelAtPeriodEnd === true || billing.switchingToWallet);
204
+ }
205
+ function walletSwitchNote(billing, price) {
206
+ return `When this period ends, the plan moves to wallet billing and renews from ${walletName(billing)}${price !== null ? ` at ${formatUsd(price)} a month` : ''}.`;
207
+ }
208
+ // A card subscription that ends with this period is not renewed by card, so its notes point to no next invoice.
209
+ const CARD_ENDS_QUOTE = 'Nothing is charged now. The card subscription is updated to the new price for the rest of this period.';
210
+ // 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.
211
+ 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.';
212
+ /** 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). */
213
+ function renewsBack(billing) {
214
+ return billing.plan === 'prepaid' && billing.status === 'expired';
215
+ }
216
+ /** What a price drop on a wallet plan gives back now, from Wiro's own plan change. */
217
+ function walletCreditNote(change) {
218
+ const used = change.usedCreditsUsd ?? 0;
219
+ // A credit is at most 1 cent for each credit the change takes away unused, so one that takes none away gives nothing back.
220
+ if (change.creditNowUsd === 0 && used > 0 && change.creditsNow !== null && change.creditsNow >= 0) {
221
+ return 'Nothing comes back: the change takes no credits away.';
222
+ }
223
+ if (change.creditNowUsd === null) {
224
+ 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.';
225
+ }
226
+ if (change.creditNowUsd > 0) {
227
+ return `${formatUsd(change.creditNowUsd)} goes back to the balances that paid, and each keeps its own end date.`
228
+ + (used > 0 ? ` ${formatUsd(used)} comes off for credits already used above what the plan keeps.` : '');
229
+ }
230
+ return used > 0
231
+ ? 'Nothing comes back: the credits already used are worth more than the time left.'
232
+ : 'Nothing comes back to the wallet now.';
233
+ }
234
+ /**
235
+ * The quote's billing line. An active plan's money and credits come from the
236
+ * plan change Wiro works out on the plan as it is now (change); without it,
237
+ * the line says what happens without a number.
238
+ */
239
+ function quoteBillingNote(billing, delta, change, nextPrice, skillsOff) {
104
240
  // A card plan whose renewal is not known gets the new price without a renewal claim.
105
241
  const after = billing.cancelAtPeriodEnd === true
106
242
  ? ` ${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)}.`;
243
+ : billing.switchingToWallet
244
+ ? ` ${walletSwitchNote(billing, nextPrice)}`
245
+ : nextPrice === null
246
+ ? ''
247
+ : billing.cancelAtPeriodEnd === false
248
+ ? ` The plan then renews at ${formatUsd(nextPrice)} a month.`
249
+ : ` The new monthly price is ${formatUsd(nextPrice)}.`;
112
250
  if (billing.plan === 'none') {
113
251
  return 'The agent has no subscription yet, so nothing is charged now. The new monthly price applies once it is subscribed.';
114
252
  }
115
253
  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.`;
254
+ return renewsBack(billing)
255
+ ? `The subscription is ${billing.status ?? 'not active'}. Skills can only change on an active subscription, so renew it with manage_agent_billing first.`
256
+ : `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
257
  }
118
- if (delta === null) {
258
+ if (delta !== null && Math.abs(delta) < 0.005)
259
+ return 'The monthly price does not change, so nothing is charged.';
260
+ if (delta === null || change === null) {
119
261
  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}`;
262
+ ? `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}`
263
+ : cardEndsWithPeriod(billing)
264
+ ? `${CARD_ENDS_QUOTE}${after}`
265
+ : billing.cancelAtPeriodEnd === null
266
+ ? `${CARD_UNKNOWN_QUOTE}${after}`
267
+ : `Any prorated difference appears on the next invoice.${after}`;
122
268
  }
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}`;
269
+ const ends = creditsEndNote(skillsOff, change.creditsNow, change.creditsThisPeriod);
270
+ // Credits come with a rise when this period gains them. A plan priced again at today's rates can also
271
+ // lose credits, or keep them, while its price rises.
272
+ const gained = change.creditsNow !== null && change.creditsNow > 0 ? change.creditsNow : null;
273
+ const notGained = change.creditsNow !== null && change.creditsNow <= 0 ? ` ${ends}` : '';
274
+ if (!change.wallet) {
275
+ if (delta > 0) {
276
+ const added = gained !== null ? ` ${formatCredits(gained)} credits come now for the rest of this period.` : notGained;
277
+ if (cardEndsWithPeriod(billing))
278
+ return `${CARD_ENDS_QUOTE}${added}${after}`;
279
+ if (billing.cancelAtPeriodEnd === null)
280
+ return `${CARD_UNKNOWN_QUOTE}${added}${after}`;
281
+ return gained !== null
282
+ ? `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}`
283
+ : `Nothing is charged now. The prorated difference appears on the next invoice.${notGained}${after}`;
284
+ }
285
+ if (cardEndsWithPeriod(billing))
286
+ return `${CARD_ENDS_QUOTE} ${ends}${after}`;
287
+ if (billing.cancelAtPeriodEnd === null)
288
+ return `${CARD_UNKNOWN_QUOTE} ${ends}${after}`;
289
+ return `The prorated difference comes off the next invoice. ${ends}${after}`;
127
290
  }
128
- const days = billing.daysRemaining !== null ? ` for the ${billing.daysRemaining} days left in this period` : ' for the rest of this period';
129
291
  if (delta > 0) {
130
- return `About ${formatUsd(estimate ?? 0)} is taken from ${walletName(billing)} now${days}.${after}`;
292
+ const charge = formatUsd(change.chargeNowUsd ?? 0);
293
+ const credits = gained !== null ? `, and ${formatCredits(gained)} credits come with it` : '';
294
+ const days = daysLeftText(change.secondsLeft);
295
+ return underOneDayLeft(change.secondsLeft)
296
+ ? `${charge} is taken from ${walletName(billing)} now at the one-day minimum${credits}.${notGained}${after}`
297
+ : `${charge} is taken from ${walletName(billing)} now for the rest of this period${days ? ` (${days})` : ''}${credits}.${notGained}${after}`;
131
298
  }
132
- return `About ${formatUsd(Math.abs(estimate ?? 0))} is credited back to ${walletName(billing)} now${days}. ${CREDIT_SPEND_BY}${after}`;
299
+ return `${walletCreditNote(change)} ${ends}${after}`;
133
300
  }
134
301
  function chargeNote(billing, body) {
135
302
  const charged = toNumber(body.prepaidWalletDelta);
303
+ const used = toNumber(body.usedCreditsUsd) ?? 0;
304
+ const creditsNow = isPlainObject(body.pricing) ? toNumber(body.pricing.creditsNow) : null;
136
305
  const cancelled = billing.cancelAtPeriodEnd === true && billing.status === 'active' ? ` ${cancelledNote(billing)}` : '';
137
306
  if (charged !== null && charged > 0) {
138
- return { charged, note: `${formatUsd(charged)} was taken from ${walletName(billing)} for the rest of this period.${cancelled}` };
307
+ const credits = creditsNow !== null && creditsNow > 0 ? `, and ${formatCredits(creditsNow)} credits came with it` : '';
308
+ return { charged, note: `${formatUsd(charged)} was taken from ${walletName(billing)} for the rest of this period${credits}.${cancelled}` };
139
309
  }
140
310
  if (charged !== null && charged < 0) {
141
- return { charged, note: `${formatUsd(Math.abs(charged))} was credited to ${walletName(billing)}. ${CREDIT_SPEND_BY}${cancelled}` };
311
+ const terms = creditTerms(body.prepaidCreditParts);
312
+ const usedNote = used > 0 ? ` ${formatUsd(used)} came off for credits already used above what the plan keeps.` : '';
313
+ return { charged, note: `${formatUsd(Math.abs(charged))} was credited to ${walletName(billing)}.${terms ? ` ${terms}` : ''}${usedNote}${cancelled}` };
314
+ }
315
+ // A change that took no credits away had nothing to credit, and nothing failed.
316
+ if (billing.plan === 'prepaid' && used > 0 && creditsNow !== null && creditsNow >= 0) {
317
+ return { charged: null, note: `Nothing came back: the change took no credits away.${cancelled}` };
318
+ }
319
+ // The credits already used came to more than the time left was worth: no credit, and nothing failed.
320
+ // A part Wiro could not add (prepaidCreditFailedUsd) was due all the same: its warning follows, and the
321
+ // credits used took only what Wiro names.
322
+ if (billing.plan === 'prepaid' && used > 0) {
323
+ return (toNumber(body.prepaidCreditFailedUsd) ?? 0) > 0
324
+ ? { charged: null, note: `Nothing came back now. ${formatUsd(used)} came off for credits already used above what the plan keeps.${cancelled}` }
325
+ : { charged: null, note: `Nothing came back: the credits already used were worth more than the time left.${cancelled}` };
142
326
  }
143
327
  if (body.stripeProrationApplied === true) {
144
- return { charged: null, note: `The card subscription was updated; any prorated difference appears on the next invoice.${cancelled}` };
328
+ const moved = isPlainObject(body.pricing) ? toNumber(body.pricing.deltaUsd) : null;
329
+ if (moved !== null && Math.abs(moved) < 0.005) {
330
+ return { charged: null, note: `The monthly price did not change, so nothing was charged.${cancelled}` };
331
+ }
332
+ const added = moved !== null && moved > 0 && creditsNow !== null && creditsNow > 0
333
+ ? ` ${formatCredits(creditsNow)} credits came now for the rest of this period.`
334
+ : '';
335
+ if (cardEndsWithPeriod(billing)) {
336
+ const price = isPlainObject(body.pricing) ? toNumber(body.pricing.newPriceUsd) : null;
337
+ const switching = billing.switchingToWallet ? ` ${walletSwitchNote(billing, price)}` : '';
338
+ 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}` };
339
+ }
340
+ if (billing.cancelAtPeriodEnd === null) {
341
+ 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}` };
342
+ }
343
+ return { charged: null, note: `The card subscription was updated; any prorated difference appears on the next invoice.${added}${cancelled}` };
145
344
  }
146
345
  if (billing.plan === 'none') {
147
346
  return { charged: null, note: 'Nothing was charged: the agent has no subscription yet.' };
@@ -156,21 +355,101 @@ function appliedBillingNote(billing, body) {
156
355
  .filter((warning) => Boolean(warning));
157
356
  return warnings.length > 0 ? { ...result, note: [result.note, ...warnings].join(' ') } : result;
158
357
  }
159
- function listLine(label, names) {
160
- return names.length > 0 ? `**${label}:** ${names.join(', ')}` : null;
358
+ /** The sentence for a change whose skills need a Wiro project. */
359
+ function bindProjectNote(needed) {
360
+ return needed.some(entry => entry.credentials.includes(WIRO_PROJECT_CREDENTIAL))
361
+ ? ' bind_agent_project connects a Wiro project, with no key to paste.'
362
+ : '';
363
+ }
364
+ /** How to give the new skills what they still need. A Wiro project comes first, since it takes no key. */
365
+ function stillNeededSteps(needed) {
366
+ const keys = needed.flatMap(entry => entry.credentials);
367
+ const fields = 'set_agent_credentials or connect_agent_oauth.';
368
+ if (!keys.includes(WIRO_PROJECT_CREDENTIAL))
369
+ return `Set them with ${fields}`;
370
+ const others = keys.some(key => key !== WIRO_PROJECT_CREDENTIAL) ? ` Set the others with ${fields}` : '';
371
+ return `Connect the Wiro project with bind_agent_project, with no key to paste.${others}`;
372
+ }
373
+ /** A list of skills by title, with the names apply_agent_skills takes. */
374
+ function listLine(label, names, titles) {
375
+ return names.length > 0 ? `**${label}:** ${names.map(name => skillLabel(name, titles)).join(', ')}` : null;
376
+ }
377
+ /** Each new skill with the credentials it still needs, by title: Instagram Post (`int-instagram-post`): Instagram (`instagram`). */
378
+ function neededText(needed, row, titles, serviceTitles) {
379
+ return needed.map(entry => `${skillLabel(entry.skill, titles)}: ${entry.credentials.map(key => credentialLabel(row, key, serviceTitles)).join(', ')}`).join('; ');
380
+ }
381
+ /**
382
+ * The scheduled tasks a change stops or starts, as Wiro works them out with its own gates: null when Wiro
383
+ * could not tell (it sends null or nothing), undefined when there are none.
384
+ */
385
+ function scheduledTasksOf(value) {
386
+ if (!isPlainObject(value))
387
+ return null;
388
+ const stop = toStringArray(value.stop);
389
+ const resume = toStringArray(value.resume);
390
+ return stop.length > 0 || resume.length > 0 ? { stop, resume } : undefined;
391
+ }
392
+ /**
393
+ * The quote's lines for the scheduled tasks the change stops or starts, or that Wiro could not tell. Wiro
394
+ * counts a task as running by its gates, so on an agent that is not running it is a task that would run once
395
+ * the agent starts. Each task is named as people see it.
396
+ */
397
+ function movedTaskLines(moved, row, running) {
398
+ if (moved === null) {
399
+ return ['**Scheduled tasks:** Wiro could not tell which scheduled tasks this change stops or starts. Once it is applied, the result names the tasks it paused.'];
400
+ }
401
+ if (!moved)
402
+ return [];
403
+ const entries = new Map(customSkillEntries(row).map(entry => [entry.key, entry]));
404
+ const names = (list) => list.map(key => {
405
+ const entry = entries.get(key);
406
+ return taskLabel(key, entry ? taskNamedByWiro(entry) : true);
407
+ }).join(', ');
408
+ const lines = [];
409
+ if (moved.stop.length > 0) {
410
+ const one = moved.stop.length === 1;
411
+ const now = running
412
+ ? (one ? 'It runs now and stops with this change.' : 'They run now and stop with this change.')
413
+ : (one ? 'It would run once the agent starts, and this change stops it.' : 'They would run once the agent starts, and this change stops them.');
414
+ lines.push(`**Scheduled tasks that stop:** ${names(moved.stop)}. ${now} ${one ? 'It keeps its' : 'They keep their'} text, schedule and on/off state, and get_my_agent then shows what ${one ? 'it waits' : 'each waits'} for.`);
415
+ }
416
+ if (moved.resume.length > 0) {
417
+ const one = moved.resume.length === 1;
418
+ const after = running ? 'once this change is applied' : 'once this change is applied and the agent starts';
419
+ lines.push(`**Scheduled tasks that start running:** ${names(moved.resume)}. ${one ? 'It does not run now and runs' : 'They do not run now and run'} ${after}.`);
420
+ }
421
+ return lines;
422
+ }
423
+ /**
424
+ * The scheduled tasks the change paused: on and paused in the agent read after it, not paused before it, and
425
+ * among those Wiro said the change stops (stops), when it could tell.
426
+ */
427
+ function newlyPaused(before, after, stops) {
428
+ if (!after)
429
+ return [];
430
+ const pausedBefore = new Set(customSkillEntries(before).filter(entry => entry.skillOff).map(entry => entry.key));
431
+ return customSkillEntries(after).filter(entry => entry.kind === 'scheduled_task' && entry.enabled && entry.skillOff
432
+ && !pausedBefore.has(entry.key) && (stops === null || stops.includes(entry.key)));
161
433
  }
162
- /** Whether the agent's live skills and tier are what this change asked for; null when that cannot be read. */
163
- async function changeHolds(client, agentGuid, requested, tier, signal) {
434
+ /**
435
+ * The agent read again after the change, and whether its live skills and tier are what this change asked
436
+ * for; both null when it cannot be read.
437
+ */
438
+ async function readAfterChange(client, agentGuid, requested, tier, signal) {
164
439
  let live;
165
440
  try {
166
441
  live = await readUserAgent(client, agentGuid, signal);
167
442
  }
168
443
  catch {
169
- return null;
444
+ return { holds: null, live: null };
170
445
  }
171
446
  const enabled = new Set(enabledSkillNames(live));
172
447
  const skillsHold = Object.entries(requested).every(([name, on]) => enabled.has(name) === on);
173
- return skillsHold && (!tier || asString(live.tier) === tier);
448
+ return { holds: skillsHold && (!tier || asString(live.tier) === tier), live };
449
+ }
450
+ /** The line that names the tasks a change paused, with the skills each waits for. */
451
+ async function pausedLine(client, paused, signal) {
452
+ return paused.length > 0 ? `**Paused by this change:**${skillOffTasksNote(paused, await pausedSkillTitles(client, paused, signal), 'is paused')}` : null;
174
453
  }
175
454
  export function registerApplyAgentSkills(server, client) {
176
455
  server.registerTool('apply_agent_skills', {
@@ -180,21 +459,26 @@ export function registerApplyAgentSkills(server, client) {
180
459
  + 'On a team agent only a team admin or the person who deployed the agent can change its skills; other '
181
460
  + 'members are refused and nothing is charged. '
182
461
  + '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 '
462
+ + 'depend on, and returns the current and new monthly price, what Wiro charges or credits now, '
463
+ + 'the credits this period gains or loses, the scheduled tasks it stops or starts, and an idempotency_key. Nothing changes yet. 2) Show the price to the '
185
464
  + '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 '
465
+ + 'The key is tied to the quoted prices and, on a prepaid plan, the billing period: if '
466
+ + '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
467
  + '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 '
468
+ + 'for the time left in the period is taken from the wallet at once, and the added credits come '
469
+ + 'for that same time; every renewal gives all of them. When the price drops, what the time left '
470
+ + 'is worth goes back to the balances that paid, less 1 cent for each credit already used above '
471
+ + 'what the plan keeps, and each balance keeps its own end date. A skill that comes off loses its '
472
+ + 'credits for the time left; the ones the time already passed paid for stay until the renewal. '
473
+ + 'On a card plan nothing is charged at once: the card subscription is updated to the new price, '
474
+ + 'a plan that renews by card shows the difference on its next invoice, and the added credits come '
475
+ + 'for the time left. A running agent restarts once when a skill turns on or off; a change of the '
476
+ + 'tier alone leaves it running. Newly enabled skills may still need credentials '
477
+ + '(set_agent_credentials, connect_agent_oauth, bind_agent_project for a Wiro project) and do not create scheduled tasks '
194
478
  + '(manage_agent_custom_skill does). Find skill names with list_agent_skills and the '
195
479
  + 'agent\'s current skills with get_my_agent. The agents playbook (get_playbook) has the '
196
480
  + 'full setup walkthrough.',
197
- inputSchema: {
481
+ inputSchema: strictInput({
198
482
  agent_guid: z.string().min(1).describe('The deployed agent\'s guid, from list_my_agents.'),
199
483
  skills: z.record(z.string(), z.boolean())
200
484
  .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 +489,12 @@ export function registerApplyAgentSkills(server, client) {
205
489
  confirm: z.boolean().default(false)
206
490
  .describe('false (default) only returns the quote. true applies the change and charges it; it needs the idempotency_key from the quote.'),
207
491
  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
- },
492
+ .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.'),
493
+ }),
210
494
  outputSchema,
211
495
  annotations: {
212
496
  readOnlyHint: false,
213
- destructiveHint: false,
497
+ destructiveHint: true,
214
498
  idempotentHint: true,
215
499
  openWorldHint: true,
216
500
  },
@@ -220,6 +504,7 @@ export function registerApplyAgentSkills(server, client) {
220
504
  return errorResult(new Error('Nothing to change: pass at least one skill or a tier.'));
221
505
  }
222
506
  const registry = await loadSkillRegistry(client, extra.signal);
507
+ const titles = registryTitles(registry);
223
508
  const plan = planSkillChanges(skills, registry, add_dependencies);
224
509
  if (plan.errors.length > 0) {
225
510
  return errorResult(new Error(`The change was not sent:\n\n- ${plan.errors.join('\n- ')}`));
@@ -241,10 +526,13 @@ export function registerApplyAgentSkills(server, client) {
241
526
  const check = checkSkillSet(enabledNow, diff.changes, registry);
242
527
  if (check.deps.length > 0 || check.conflicts.length > 0) {
243
528
  const problems = [
244
- ...check.deps.map(dep => (diff.disable.includes(dep.requires)
245
- ? `${dep.skill} needs ${dep.requires}. Keep ${dep.requires} on, or also turn off ${dep.skill}.`
246
- : `${dep.skill} needs ${dep.requires}, which is off. Turn ${dep.requires} on in the same change.`)),
247
- ...check.conflicts.map(pair => `${pair.a} and ${pair.b} cannot be on together. Keep only one.`),
529
+ ...check.deps.map(dep => {
530
+ const [skill, requires] = [skillLabel(dep.skill, titles), skillLabel(dep.requires, titles)];
531
+ return diff.disable.includes(dep.requires)
532
+ ? `${skill} needs ${requires}. Keep ${requires} on, or also turn off ${skill}.`
533
+ : `${skill} needs ${requires}, which is off. Turn ${requires} on in the same change.`;
534
+ }),
535
+ ...check.conflicts.map(pair => `${skillLabel(pair.a, titles)} and ${skillLabel(pair.b, titles)} cannot be on together. Keep only one.`),
248
536
  ];
249
537
  return errorResult(new Error(`The change was not sent:\n\n- ${[...new Set(problems)].join('\n- ')}`));
250
538
  }
@@ -257,7 +545,10 @@ export function registerApplyAgentSkills(server, client) {
257
545
  alreadySet: diff.alreadySet.filter(name => !plan.addedDependencies.includes(name)),
258
546
  };
259
547
  const credentialsNeeded = credentialsNeededFor(diff.enable, registry, row);
260
- const nothingToDo = Object.keys(diff.changes).length === 0 && !tierChange;
548
+ const serviceTitles = await credentialTitles(client, credentialsNeeded.flatMap(entry => entry.credentials), extra.signal);
549
+ // A running agent restarts only for a skill that turns on or off.
550
+ const skillsMove = Object.keys(diff.changes).length > 0;
551
+ const nothingToDo = !skillsMove && !tierChange;
261
552
  const base = {
262
553
  agentGuid: agent_guid,
263
554
  changes,
@@ -281,7 +572,8 @@ export function registerApplyAgentSkills(server, client) {
281
572
  };
282
573
  }
283
574
  const currentPrice = toNumber(row.monthlypriceusd);
284
- const currentCredits = toNumber(row.monthlycredits);
575
+ // The plan's monthly figure is what each renewal grants. This period's can differ after a change inside it.
576
+ const currentCredits = toNumber(row.renewalcredits) ?? toNumber(row.monthlycredits);
285
577
  const key = idempotency_key?.trim() ?? '';
286
578
  // Every team member can read a team agent and price a change, but only a
287
579
  // team admin or the person who deployed it can apply one. The credit
@@ -304,29 +596,34 @@ export function registerApplyAgentSkills(server, client) {
304
596
  isError: true,
305
597
  };
306
598
  }
599
+ // The tasks Wiro says this change stops: null until it says, and when it could not tell.
600
+ let stops = null;
307
601
  if (!nothingToDo) {
308
602
  const preview = requireApiSuccess(await client.post('/UserAgent/PricingPreview', {
309
603
  useragentguid: agent_guid,
310
604
  skillOverrides: diff.changes,
311
605
  ...(tierChange ? { tier: tierChange } : {}),
606
+ withScheduledTasks: true,
312
607
  }, extra.signal), 'Could not price this change.');
608
+ const moved = scheduledTasksOf(preview.scheduledTasks);
609
+ stops = moved === null ? null : moved?.stop ?? [];
313
610
  const newPrice = toNumber(preview.totalPriceUsd);
314
611
  if (newPrice === null) {
315
612
  throw new Error('Wiro did not return the new monthly price of this change, so it cannot be quoted. Try again later.');
316
613
  }
317
- const priceTag = skillPriceTag(currentPrice, newPrice);
614
+ const priceTag = skillPriceTag(currentPrice, newPrice, billing.plan === 'prepaid' ? billing.periodEnd : null);
318
615
  const approved = confirm && givenKey !== null && givenKey.priceTag === priceTag;
319
616
  if (!approved) {
320
617
  const newCredits = toNumber(preview.totalMonthlyCredits);
321
618
  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);
619
+ // What Wiro works out for this change on the plan as it is now. Only an active plan has one.
620
+ const change = billing.plan !== 'none' && billing.status === 'active' ? readPlanChange(preview.planChange) : null;
621
+ const note = quoteBillingNote(billing, delta, change, newPrice, changes.disable.length);
622
+ const walletDrop = change !== null && change.wallet && delta !== null && delta < 0;
326
623
  const quoteKey = givenKey?.priceTag === priceTag ? key : newSkillChangeKey(digest, priceTag);
327
624
  const priceChanged = confirm && givenKey !== null;
328
625
  const blocked = billing.plan !== 'none' && billing.status !== 'active';
329
- const restart = restartNote(status, 'skill change');
626
+ const restart = skillsMove ? restartNote(status, 'skill change') : TIER_ONLY_RESTART;
330
627
  const priceNote = todayPriceNote(row, billing, currentPrice, currentCredits, tierChange);
331
628
  const pricing = {
332
629
  currentMonthlyUsd: currentPrice,
@@ -335,12 +632,13 @@ export function registerApplyAgentSkills(server, client) {
335
632
  currentMonthlyCredits: currentCredits,
336
633
  newMonthlyCredits: newCredits,
337
634
  deltaMonthlyCredits: difference(newCredits, currentCredits),
635
+ ...periodFields(change),
338
636
  ...(priceNote ? { note: priceNote } : {}),
339
637
  };
340
638
  const nextAction = blocked
341
639
  ? {
342
640
  tool: 'manage_agent_billing',
343
- arguments: { agent_guid, action: 'renew' },
641
+ arguments: { agent_guid, action: renewsBack(billing) ? 'renew' : 'subscribe' },
344
642
  reason: 'The subscription must be active before skills can change.',
345
643
  }
346
644
  : {
@@ -360,29 +658,35 @@ export function registerApplyAgentSkills(server, client) {
360
658
  const lead = !confirm
361
659
  ? null
362
660
  : 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.'
661
+ ? '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
662
  : '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
663
  const lines = [
366
664
  '## Skill change quote',
367
665
  '',
368
666
  ...(lead ? [lead, ''] : []),
369
667
  `**Agent:** \`${agent_guid}\` (${agentStatusLabel(status)})`,
370
- listLine('Turn on', changes.enable),
371
- listLine('Added because other skills need them', changes.addedDependencies),
372
- listLine('Turn off', changes.disable),
373
- listLine('Already as requested', changes.alreadySet),
668
+ listLine('Turn on', changes.enable, titles),
669
+ listLine('Added because other skills need them', changes.addedDependencies, titles),
670
+ listLine('Turn off', changes.disable, titles),
671
+ listLine('Already as requested', changes.alreadySet, titles),
374
672
  tierChange ? `**Plan:** ${currentTier ?? 'unknown'} to ${tierChange}` : null,
375
673
  `**Monthly price:** ${currentPrice !== null ? formatUsd(currentPrice) : 'unknown'} now, ${formatUsd(newPrice)} after the change${delta !== null ? ` (${delta >= 0 ? '+' : ''}${formatUsd(delta)})` : ''}`,
376
674
  priceNote ? `**Price update:** ${priceNote}` : null,
377
- newCredits !== null ? `**Monthly credits:** ${currentCredits ?? 'unknown'} now, ${newCredits} after the change` : null,
675
+ change?.creditsFromRenewal !== null && change?.creditsFromRenewal !== undefined
676
+ ? `**Monthly credits:** ${currentCredits !== null ? formatCredits(currentCredits) : 'unknown'} now, ${formatCredits(change.creditsFromRenewal)} ${renewalWhen(billing)}`
677
+ : newCredits !== null ? `**Monthly credits:** ${currentCredits !== null ? formatCredits(currentCredits) : 'unknown'} now, ${formatCredits(newCredits)} ${creditsWhen(billing)}` : null,
678
+ change && change.creditsNow !== null && change.creditsThisPeriod !== null
679
+ ? `**This period:** ${signedCredits(change.creditsNow)} credits now, ${formatCredits(change.creditsThisPeriod)} in all until ${unixToIso(change.periodEnd) ?? 'the period ends'}`
680
+ : null,
378
681
  `**Billing:** ${note}`,
379
682
  `**Restart:** ${restart}`,
683
+ ...movedTaskLines(moved, row, running),
380
684
  credentialsNeeded.length > 0
381
- ? `**Credentials the new skills need:** ${credentialsNeeded.map(entry => `${entry.skill} (${entry.credentials.join(', ')})`).join('; ')}`
685
+ ? `**Credentials the new skills need:** ${neededText(credentialsNeeded, row, titles, serviceTitles)}.${bindProjectNote(credentialsNeeded)}`
382
686
  : null,
383
687
  '',
384
688
  blocked
385
- ? 'Renew the subscription with manage_agent_billing before applying this change.'
689
+ ? `${renewsBack(billing) ? 'Renew the subscription' : 'Start a new plan'} with manage_agent_billing before applying this change.`
386
690
  : `Nothing has changed yet. After the user approves, call apply_agent_skills again with confirm: true and idempotency_key \`${quoteKey}\`.`,
387
691
  ].filter((line) => line !== null);
388
692
  return {
@@ -393,8 +697,14 @@ export function registerApplyAgentSkills(server, client) {
393
697
  idempotencyKey: quoteKey,
394
698
  ...(priceChanged ? { priceChanged: true } : {}),
395
699
  pricing,
396
- billing: { ...billingBase, estimatedChargeNowUsd: estimate, note },
700
+ billing: {
701
+ ...billingBase,
702
+ estimatedChargeNowUsd: change ? change.chargeNowUsd : null,
703
+ ...(walletDrop ? { creditNowUsd: change.creditNowUsd, usedCreditsUsd: change.usedCreditsUsd } : {}),
704
+ note,
705
+ },
397
706
  ...(Array.isArray(preview.enabledSkills) ? { enabledSkills: preview.enabledSkills.map(String) } : {}),
707
+ ...(moved !== undefined ? { scheduledTasks: moved } : {}),
398
708
  restart,
399
709
  nextAction,
400
710
  },
@@ -415,9 +725,16 @@ export function registerApplyAgentSkills(server, client) {
415
725
  if (extra.signal.aborted)
416
726
  throw error;
417
727
  if (!outcomeUnknown(error)) {
418
- const failure = describeSkillsApplyError(error, billing.plan);
728
+ const failure = describeSkillsApplyError(error, billing.plan, titles);
729
+ // Code 110 is the whole outcome: the next step reads the agent, and the call is not repeated.
730
+ const failedChange = failure.code === 110;
731
+ const nextAction = failedChange
732
+ ? { tool: 'get_my_agent', arguments: { agent_guid }, reason: 'Check the agent\'s skills and monthly price. Do not repeat the change.' }
733
+ : failure.code === 107
734
+ ? { tool: 'apply_agent_skills', arguments: { agent_guid, skills, ...(tier ? { tier } : {}), add_dependencies }, reason: 'Get a new quote for this change, with a new key.' }
735
+ : undefined;
419
736
  const text = [
420
- '## Skill change not applied',
737
+ failedChange ? '## Skill change failed' : '## Skill change not applied',
421
738
  '',
422
739
  failure.message,
423
740
  failure.guidance ? `\n${failure.guidance}` : '',
@@ -430,7 +747,7 @@ export function registerApplyAgentSkills(server, client) {
430
747
  status: 'refused',
431
748
  idempotencyKey: key,
432
749
  billing: { ...billingBase, note: failure.guidance || failure.message },
433
- restart: UNCHANGED_RESTART,
750
+ restart: failedChange ? 'The agent does not restart for this change.' : UNCHANGED_RESTART,
434
751
  error: {
435
752
  code: failure.code,
436
753
  message: failure.message,
@@ -439,6 +756,7 @@ export function registerApplyAgentSkills(server, client) {
439
756
  ...(failure.requiredUsd !== undefined ? { requiredUsd: failure.requiredUsd } : {}),
440
757
  ...(failure.availableUsd !== undefined ? { availableUsd: failure.availableUsd } : {}),
441
758
  },
759
+ ...(nextAction ? { nextAction } : {}),
442
760
  },
443
761
  isError: true,
444
762
  };
@@ -446,7 +764,9 @@ export function registerApplyAgentSkills(server, client) {
446
764
  const message = errorText(error);
447
765
  if (sentEmpty) {
448
766
  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.';
767
+ + (billing.plan === 'prepaid'
768
+ ? `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.`
769
+ : 'Any price difference follows the plan\'s usual billing.');
450
770
  return {
451
771
  content: [{ type: 'text', text: `## No change needed\n\n${note}` }],
452
772
  structuredContent: {
@@ -460,25 +780,28 @@ export function registerApplyAgentSkills(server, client) {
460
780
  }
461
781
  // The agent is read again to see whether the change is in place
462
782
  // before anything is reported.
463
- const holds = await changeHolds(client, agent_guid, plan.requested, tierChange, extra.signal);
783
+ const { holds, live } = await readAfterChange(client, agent_guid, plan.requested, tierChange, extra.signal);
464
784
  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.'}`;
785
+ const paused = newlyPaused(row, live, stops);
786
+ 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
787
  return {
467
788
  content: [{ type: 'text', text: [
468
789
  '## Skills updated',
469
790
  '',
470
791
  `**Agent:** \`${agent_guid}\``,
471
- listLine('Turned on', changes.enable),
472
- listLine('Turned off', changes.disable),
792
+ listLine('Turned on', changes.enable, titles),
793
+ listLine('Turned off', changes.disable, titles),
473
794
  `**Billing:** ${note}`,
474
- `**Restart:** ${restartNote(status, 'skill change')}`,
795
+ `**Restart:** ${skillsMove ? restartNote(status, 'skill change') : TIER_ONLY_RESTART}`,
796
+ await pausedLine(client, paused, extra.signal),
475
797
  ].filter((line) => line !== null).join('\n') }],
476
798
  structuredContent: {
477
799
  ...base,
478
800
  status: 'applied',
479
801
  idempotencyKey: key,
480
802
  billing: { ...billingBase, chargedNowUsd: null, note },
481
- restart: restartNote(status, 'skill change'),
803
+ restart: skillsMove ? restartNote(status, 'skill change') : TIER_ONLY_RESTART,
804
+ ...(paused.length > 0 ? { pausedTasks: paused.map(entry => entry.key) } : {}),
482
805
  },
483
806
  };
484
807
  }
@@ -493,7 +816,7 @@ export function registerApplyAgentSkills(server, client) {
493
816
  status: 'refused',
494
817
  idempotencyKey: key,
495
818
  billing: { ...billingBase, note },
496
- restart: unverified ? 'The agent restarts if the change was saved.' : UNCHANGED_RESTART,
819
+ restart: !unverified ? UNCHANGED_RESTART : skillsMove ? 'The agent restarts if the change was saved.' : TIER_ONLY_RESTART,
497
820
  error: { code: null, message },
498
821
  },
499
822
  isError: true,
@@ -501,7 +824,7 @@ export function registerApplyAgentSkills(server, client) {
501
824
  }
502
825
  // The live agent is checked against this change before it is reported
503
826
  // as applied.
504
- const holds = await changeHolds(client, agent_guid, plan.requested, tierChange, extra.signal);
827
+ const { holds, live } = await readAfterChange(client, agent_guid, plan.requested, tierChange, extra.signal);
505
828
  if (holds === false) {
506
829
  const note = 'The agent\'s skills do not match this change, so it is not in place. Check get_my_agent; if they '
507
830
  + 'still do not match the request, call apply_agent_skills again without idempotency_key to get a new quote.';
@@ -527,6 +850,13 @@ export function registerApplyAgentSkills(server, client) {
527
850
  // result of the earlier call with this key, not a new charge.
528
851
  const replayed = sentEmpty && !applied.noOp;
529
852
  const serverPricing = applied.pricing ?? {};
853
+ // An agent with no plan has no period and no renewal yet, as its quote says.
854
+ const appliedChange = billing.plan === 'none' ? null : readPlanChange({
855
+ creditsNow: serverPricing.creditsNow,
856
+ creditsThisPeriod: serverPricing.creditsThisPeriod,
857
+ creditsFromRenewal: serverPricing.creditsFromRenewal,
858
+ periodEnd: serverPricing.periodEnd,
859
+ });
530
860
  const pricing = applied.noOp
531
861
  ? undefined
532
862
  : {
@@ -536,46 +866,58 @@ export function registerApplyAgentSkills(server, client) {
536
866
  currentMonthlyCredits: toNumber(serverPricing.previousMonthlyCredits),
537
867
  newMonthlyCredits: toNumber(serverPricing.newMonthlyCredits),
538
868
  deltaMonthlyCredits: toNumber(serverPricing.deltaCredits),
869
+ ...periodFields(appliedChange),
539
870
  };
540
871
  const charge = appliedBillingNote(billing, applied);
872
+ const usedCreditsUsd = toNumber(applied.usedCreditsUsd);
541
873
  const billingNote = replayed
542
874
  ? `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
875
  : charge.note;
876
+ // Wiro says whether this change restarts a running agent; an answer without it goes by the skills that move.
877
+ const restarts = typeof applied.restartTriggered === 'boolean' ? applied.restartTriggered : skillsMove;
544
878
  const restart = applied.noOp || replayed
545
879
  ? UNCHANGED_RESTART
546
- : restartNote(status, 'skill change');
880
+ : restarts ? restartNote(status, 'skill change') : TIER_ONLY_RESTART;
547
881
  const firstCredential = credentialsNeeded[0]?.credentials[0];
548
882
  const nextAction = applied.noOp || replayed
549
883
  ? 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
884
+ : firstCredential === WIRO_PROJECT_CREDENTIAL
885
+ ? bindProjectStep(agent_guid, 'Connect the Wiro project the new skills need.')
886
+ : firstCredential
557
887
  ? {
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.',
888
+ tool: 'set_agent_credentials',
889
+ arguments: { agent_guid, credential: firstCredential },
890
+ reason: `See what ${firstCredential} needs before asking the user for it.`,
561
891
  }
562
- : undefined;
892
+ : running && restarts
893
+ ? {
894
+ tool: 'get_my_agent',
895
+ arguments: { agent_guid, wait_seconds: DEFAULT_WAIT_SECONDS },
896
+ reason: 'Check that the agent is running again with the new skills.',
897
+ }
898
+ : undefined;
899
+ // Read from the agent after the change: the tasks it paused, which the quote named among those that stop.
900
+ const paused = applied.noOp || replayed ? [] : newlyPaused(row, live, stops);
563
901
  const lines = [
564
902
  applied.noOp ? '## No change needed' : replayed ? '## Already applied' : '## Skills updated',
565
903
  '',
566
904
  `**Agent:** \`${agent_guid}\``,
567
- listLine('Turned on', changes.enable),
568
- listLine('Turned off', changes.disable),
569
- applied.noOp || replayed ? listLine('Already as requested', changes.alreadySet) : null,
905
+ listLine('Turned on', changes.enable, titles),
906
+ listLine('Turned off', changes.disable, titles),
907
+ applied.noOp || replayed ? listLine('Already as requested', changes.alreadySet, titles) : null,
570
908
  tierChange && !applied.noOp ? `**Plan:** now ${asString(applied.tier) ?? tierChange}` : null,
571
909
  pricing?.newMonthlyUsd !== undefined && pricing.newMonthlyUsd !== null
572
910
  ? `**Monthly price:** ${formatUsd(pricing.newMonthlyUsd)}${pricing.currentMonthlyUsd !== null ? ` (was ${formatUsd(pricing.currentMonthlyUsd)})` : ''}`
573
911
  : null,
912
+ pricing && appliedChange?.creditsNow !== null && appliedChange?.creditsNow !== undefined && appliedChange.creditsThisPeriod !== null
913
+ ? `**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)}` : ''}`
914
+ : null,
574
915
  `**Billing:** ${billingNote}`,
575
916
  `**Restart:** ${restart}`,
576
917
  holds === null ? 'The agent could not be read afterwards to confirm the new skills; get_my_agent shows them.' : null,
918
+ await pausedLine(client, paused, extra.signal),
577
919
  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.`
920
+ ? `**Still needed:** credentials for ${neededText(credentialsNeeded, row, titles, serviceTitles)}. ${stillNeededSteps(credentialsNeeded)}`
579
921
  : null,
580
922
  ].filter((line) => line !== null);
581
923
  return {
@@ -585,9 +927,16 @@ export function registerApplyAgentSkills(server, client) {
585
927
  status: applied.noOp ? 'no_change' : 'applied',
586
928
  idempotencyKey: key,
587
929
  ...(pricing ? { pricing } : {}),
588
- billing: { ...billingBase, chargedNowUsd: charge.charged, note: billingNote },
930
+ // The saved answer of an earlier call: its charge is in the note, and this call charged nothing.
931
+ billing: {
932
+ ...billingBase,
933
+ chargedNowUsd: replayed ? 0 : charge.charged,
934
+ ...(usedCreditsUsd !== null && usedCreditsUsd > 0 ? { usedCreditsUsd } : {}),
935
+ note: billingNote,
936
+ },
589
937
  ...(Array.isArray(serverPricing.enabledSkills) ? { enabledSkills: serverPricing.enabledSkills.map(String) } : {}),
590
938
  restart,
939
+ ...(paused.length > 0 ? { pausedTasks: paused.map(entry => entry.key) } : {}),
591
940
  ...(nextAction ? { nextAction } : {}),
592
941
  },
593
942
  };