diff --git a/.github/instructions/content-guidelines.instructions.md b/.github/instructions/content-guidelines.instructions.md
new file mode 100644
index 000000000000..0efb2d4d2344
--- /dev/null
+++ b/.github/instructions/content-guidelines.instructions.md
@@ -0,0 +1,60 @@
+---
+applyTo: "content/**,data/reusables/**"
+---
+
+# Content guidelines for docs.github.com
+
+**When to use**: Writing, editing, or reviewing documentation articles and reusable prose. These are strategic content rules: what to write and how to focus an article.
+
+When asked to work on one part of a larger article, read the whole article first so you can judge whether it meets these guidelines as a whole.
+
+**How to apply these guidelines**: Treat them as strategic suggestions to weigh per article, not mechanical rules to enforce. The right emphasis depends on the article's content type (procedural, conceptual, or reference), so use judgment and stay silent when a guideline does not cleanly apply, rather than flagging or rewriting reflexively.
+
+## Keep only essential content
+
+The strategic priority is simplification: create less content and remove content that is not essential, so readers can navigate higher-value content more easily. Flag content to trim or remove by asking:
+
+* Does it serve a large or high-value audience, rather than an edge case the company does not prioritize?
+* Does it help people use GitHub the way we want them to, rather than documenting every possible use?
+* Would a typical internet user figure this out on their own by exploring the UI?
+* Is the information presented at the moment the reader actually needs it?
+
+## Intros: pull people in
+
+This section applies mainly to the `intro` frontmatter field and, for conceptual articles, section openings.
+
+* Open with the value the reader gets, and the product that delivers it, rather than a restatement of the task or a bare feature name. Technical detail is not bad and belongs in the article; it just should not be the first thing the reader sees when a value-led opening is possible.
+* Do not repeat the wording of the title.
+* Do not start with "Learn how to..."; it buries the value.
+* When conceptual and procedural articles cover the same topic, differentiate them through sentence structure. Conceptual describes what the thing is and why it matters ("{% data variables.product.prodname_copilot %} is an AI coding assistant that helps you write code faster."). Procedural describes what the reader will do and the value they get ("Start using {% data variables.product.prodname_copilot %} to write code faster.").
+
+Examples of strong intros by content type:
+
+* **Conceptual** ("Larger runners"): "Organize and govern your workflows with larger runners using runner groups, concurrency policies, and granular access controls."
+* **Procedural** ("Running jobs on larger runners"): "Route jobs to the right machines by using runner groups and workflow labels."
+* **Reference** ("Supported AI models in {% data variables.product.prodname_copilot %}"): "Identify which AI models are supported in {% data variables.product.prodname_copilot %} for each client and plan."
+
+## Drive people to the product
+
+* Every article should move the reader to try or use the product, directly or indirectly. Even reference articles do this: readers consult them in order to use the product, so the support is built in and a separate CTA is often unnecessary.
+* Only include a CTA link when it genuinely makes the reader's task easier, for example by saving them the time of navigating to a settings page themselves. Do not force a CTA; if none would genuinely help the reader, do not add one. Avoid turning articles into clickbait.
+* A CTA can take several forms, for example a direct link to the relevant product or feature, a Copilot prompt the reader can run, or a link to start a free trial.
+* Only link to a URL that is the same for everyone on that version. Do not add a CTA when the in-product URL must include an enterprise, organization, or repository name (for example, `https://github.com/ORG/REPO/settings/copilot/code_review`), because the link cannot be made to work for all readers.
+* Procedural articles: include a CTA wherever one genuinely helps, as directly as possible.
+* Conceptual articles: point the reader to exactly one clear next step, usually a link to the related procedure (for example, an "About pull requests" article points to "Creating a pull request"). Place it where the reader is ready to act, typically at the end of the article.
+
+## Energy and tone
+
+These apply to the prose in an article (intros and explanatory text), not to structural elements like tables, procedural steps, or code.
+
+* Lead with value and real-life impact over technical detail.
+* Connect features to the reader's real-life problems to generate genuine interest.
+* Use plain, friendly, approachable language. Avoid marketing jargon, buzzwords, and inflated adjectives.
+
+## Scannability
+
+* Give each article exactly one purpose, regardless of content type. That purpose may be physical (e.g., enabling a setting), conceptual (e.g., building a mental model of what a feature does and why it matters, choosing between two options), or referential (e.g., determining which AI models are available to the reader). Include only information central to that purpose for most readers.
+* Write for the one reader scenario the article targets, for example a particular deployment configuration (GHEC with EMUs vs. Classic) or a particular type of reader (an open source maintainer vs. an enterprise developer). When the article has a content design plan, target the audience it identifies rather than inventing one; for small edits without a plan, follow the audience the existing article is clearly written for. Do not branch content to serve multiple audiences; readers in other scenarios can adapt the guidance. The exception is version differences: when in-article `{% ifversion %}` branching is genuinely required (see the versioning rules in `content.instructions.md`), it is not a scannability violation.
+* Ruthlessly minimize links. Only link when you actively want most readers to follow it in the ideal scenario. No "just in case" links. Links that build a logical user journey are exactly the kind to keep, for example a Prerequisites link that sends the reader to setup they need first, or a Next steps link that points them to the natural follow-on task.
+* Ruthlessly minimize alerts (notes, tips, warnings): more than one per article should be exceptional, and crowding several into one section is worse than spreading them out. Keep each to 1-2 sentences. Don't open an article or section with an alert unless the reader needs it before the surrounding content. Prefer folding a useful alert into the prose over deleting it, but first apply this test: if the reader must actually notice it to use the page correctly, keep it as an alert (don't fold or count it), since folding defeats its purpose. This covers, for example, critical warnings, plan or availability constraints, public preview notices, and cues that orient the reader to how the page works or which content applies to them.
+* Prefer short sentences and paragraphs, generous white space, and formatting like bold and tables to highlight key information. Use a table only for genuinely complex data that belongs in a tabular format; do not add a table that repeats information already stated more clearly in prose.
diff --git a/assets/images/help/copilot/copilot-sdk/features-agent-loop-diagram-1.png b/assets/images/help/copilot/copilot-sdk/features-agent-loop-diagram-1.png
index f2f0500a9d3e..09030de7ef9b 100644
Binary files a/assets/images/help/copilot/copilot-sdk/features-agent-loop-diagram-1.png and b/assets/images/help/copilot/copilot-sdk/features-agent-loop-diagram-1.png differ
diff --git a/assets/images/help/copilot/copilot-sdk/features-agent-loop-diagram-2.png b/assets/images/help/copilot/copilot-sdk/features-agent-loop-diagram-2.png
index 0c7b17986298..871721c4dcf0 100644
Binary files a/assets/images/help/copilot/copilot-sdk/features-agent-loop-diagram-2.png and b/assets/images/help/copilot/copilot-sdk/features-agent-loop-diagram-2.png differ
diff --git a/config/moda/secrets/ci/secrets.yml b/config/moda/secrets/ci/secrets.yml
index 0a481b8efa0a..b2c36935e4e8 100644
--- a/config/moda/secrets/ci/secrets.yml
+++ b/config/moda/secrets/ci/secrets.yml
@@ -7,3 +7,21 @@ secrets:
playbook: ''
externally_usable: true
kind: latest_at_deployment_start
+ DOCS_BOT_APP_CLIENT_ID:
+ owner: docs-engineering
+ type: github_app
+ externally_usable: false
+ playbook: ''
+ kind: latest_at_deployment_start
+ DOCS_BOT_APP_ID:
+ owner: docs-engineering
+ type: github_app
+ externally_usable: false
+ playbook: ''
+ kind: latest_at_deployment_start
+ DOCS_BOT_APP_PRIVATE_KEY:
+ owner: docs-engineering
+ type: github_app
+ externally_usable: false
+ playbook: ''
+ kind: latest_at_deployment_start
diff --git a/content/billing/concepts/budgets-and-alerts.md b/content/billing/concepts/budgets-and-alerts.md
index 69bf45c8c224..a8f3715cbeae 100644
--- a/content/billing/concepts/budgets-and-alerts.md
+++ b/content/billing/concepts/budgets-and-alerts.md
@@ -25,15 +25,23 @@ Each budget has a type and a scope that define which paid use contributes to spe
* **Type**: Defines which metered product or SKU is measured.
* **Scope**: Defines whether the budget applies to the whole account, or to a subset of repositories, organizations, cost centers (enterprise only), or users. User-scoped budgets are currently only supported for {% data variables.product.prodname_copilot_short %} {% data variables.product.prodname_ai_credits_short %}, and have three scopes:
+
* **Universal**: applies to all licensed users by default
* **Cost center user-level**: applies to every user in a cost center
* **Individual**: overrides the above for specific users
-For more information about how user-level budgets work and how they interact with other budget controls, see [AUTOTITLE](/copilot/concepts/billing/budgets-for-usage-based-billing).
+For {% data variables.product.prodname_copilot_short %}, cost centers can also have included usage controls, which cap how much of the shared {% data variables.product.prodname_ai_credits_short %} pool a cost center can use before metered usage begins. This is a separate control from the budgets and the included usage alerts described below. See [AUTOTITLE](/copilot/concepts/billing/budgets-for-usage-based-billing#included-usage-controls-for-cost-centers).
+
+## Roles and access for budgets
+
+Enterprise owners and billing managers can create and edit enterprise and cost center budgets, and they receive budget alerts by default. Organization owners can set budgets for their own organization, and personal account owners can set budgets for their own account. Each of these roles can view usage for the scopes they manage.
## Budget alerts
-You can enable alerts for budgets to receive emails when usage reaches 75%, 90%, and 100% of the budget amount. Emails are sent to account owners and billing managers by default. Additional recipients can be added as needed.
+You can enable alerts for budgets to be notified when usage reaches 75%, 90%, and 100% of the budget amount. Alerts are shown in the {% data variables.product.github %} UI and sent by email. By default, alerts go to account owners and billing managers, and you can add additional recipients as needed. Budget alerts are available for budgets scoped to your enterprise, a cost center, an organization, or a repository.
+
+> [!NOTE]
+> Alerting for user-level budgets is not consistently available in all scenarios. Don't rely on user-level budget alerts as your only signal, also monitor usage at the cost center or enterprise level.
## Included usage alerts
diff --git a/content/billing/concepts/cost-centers.md b/content/billing/concepts/cost-centers.md
index 37f7b6093315..70bf2ded608d 100644
--- a/content/billing/concepts/cost-centers.md
+++ b/content/billing/concepts/cost-centers.md
@@ -29,6 +29,14 @@ To get started with cost centers, see [AUTOTITLE](/billing/tutorials/control-cos
For more details, see [AUTOTITLE](/billing/reference/cost-center-allocation).
+## Controlling included usage
+
+For cost centers that contain {% data variables.product.prodname_copilot_short %} licenses, you can apply included usage controls in addition to budgets.
+
+{% data reusables.billing.included-usage-controls %}
+
+This is separate from a cost center budget, which caps metered charges only after the shared pool of {% data variables.product.prodname_ai_credits_short %} is exhausted. For more information, see [AUTOTITLE](/copilot/concepts/billing/budgets-for-usage-based-billing#included-usage-controls-for-cost-centers).
+
## Cost center limitations
* The maximum number of active cost centers per enterprise is 500.
@@ -36,3 +44,4 @@ For more details, see [AUTOTITLE](/billing/reference/cost-center-allocation).
* A maximum of 50 resources can be added to or removed from a cost center at a time.
* Azure subscriptions can only be added to or removed from cost centers through the UI.
* Outside collaborators or unaffiliated users can only be added to cost centers via the cost center API. For more information, see [AUTOTITLE](/billing/tutorials/control-costs-at-scale#add-resources-to-the-cost-center).
+* You can't set different budgets for teams within the same cost center. A budget applies to the whole cost center, so if two teams need separate budgets, create a separate cost center for each. Separate cost centers can share the same Azure billing identity.
diff --git a/content/billing/how-tos/products/use-cost-centers.md b/content/billing/how-tos/products/use-cost-centers.md
index 220017e565a4..077f6eabf648 100644
--- a/content/billing/how-tos/products/use-cost-centers.md
+++ b/content/billing/how-tos/products/use-cost-centers.md
@@ -36,7 +36,8 @@ When you create a cost center, you can add **organizations**, **repositories**,
1. If your account is billed to Azure, you have the option to add an Azure ID. Your credentials will be verified against Azure to ensure the Azure IDs associated to your account are available.
1. Under **Resources**, select the organizations, repositories, users, and/or enterprise teams that will be a part of the cost center.
- >[!NOTE] A resource (organization, repository, or user) can only be assigned to one cost center at a time. If you add a resource that belongs to a different cost center, it will be moved to the new cost center and you will be notified.
+ > [!NOTE]
+ > A resource (organization, repository, user, or enterprise team) can only be assigned to one cost center at a time. If you add a resource that belongs to a different cost center, it will be moved to the new cost center and you will be notified.
{% data reusables.billing.cost-center-create-button %}
diff --git a/content/billing/how-tos/set-up-budgets.md b/content/billing/how-tos/set-up-budgets.md
index 86ea0854df4f..b157779e5555 100644
--- a/content/billing/how-tos/set-up-budgets.md
+++ b/content/billing/how-tos/set-up-budgets.md
@@ -149,6 +149,11 @@ You can edit or delete a budget at any time, but you cannot change the scope of
1. In the list of budgets, click {% octicon "kebab-horizontal" aria-label="View actions" %} next to the budget you want to edit, and click **{% octicon "pencil" aria-hidden="true" aria-label="pencil" %} Edit** or **{% octicon "trash" aria-hidden="true" aria-label="trash" %} Delete**.
1. Follow the prompts.
+### Controlling included usage for a cost center
+
+Budgets cap metered charges after the shared pool of {% data variables.product.prodname_ai_credits_short %} is exhausted. To cap how much of the pool a cost center can use **before** the metered phase, use an included usage control. {% data variables.product.github %} sets the cap automatically based on the licenses assigned to the cost center, and you choose whether members are blocked or roll into paid overage when the cap is reached. See [AUTOTITLE](/copilot/concepts/billing/budgets-for-usage-based-billing#included-usage-controls-for-cost-centers) and [AUTOTITLE](/billing/concepts/cost-centers).
+> [!NOTE]
+> Enabling included usage controls does not retroactively redistribute the shared {% data variables.product.prodname_ai_credits_short %} enterprise pool. After the setting is enabled, users in the cost center share only the included {% data variables.product.prodname_ai_credits_short %} funded by licenses attributed to that cost center. When the setting is disabled, users in the cost center can continue drawing from the shared enterprise pool.
## Next steps
For {% data variables.product.prodname_copilot_short %}-specific budget guidance under usage-based billing, including user-level budgets and configuration scenarios, see [AUTOTITLE](/copilot/concepts/billing/budgets-for-usage-based-billing) and [AUTOTITLE](/copilot/tutorials/budgets/optimizing-your-budget-configuration).
diff --git a/content/billing/tutorials/control-costs-at-scale.md b/content/billing/tutorials/control-costs-at-scale.md
index 14d08085307d..574ddf0fadd7 100644
--- a/content/billing/tutorials/control-costs-at-scale.md
+++ b/content/billing/tutorials/control-costs-at-scale.md
@@ -106,7 +106,7 @@ Create one budget for each product, SKU, or group of SKUs that you want to contr
### Review existing budgets for conflicts
-After creating your cost center budgets, check existing enterprise-wide budgets to ensure they don't conflict with or override your new cost center budgets.
+After creating your cost center budgets, check existing enterprise-wide budgets to ensure they don't conflict with or override your new cost center budgets. When budgets overlap, the most restrictive one applies, so a low budget at a higher scope can block a cost center before its own budget is reached.
Navigate to the "Budgets and alerts" page. You'll see two lists of budgets:
@@ -121,6 +121,14 @@ Review whether any enterprise budgets apply to the same products or SKUs as your
Filter the other budgets list to show a scope of **Cost Centers**. You should see your new cost center with a row for each budget you created. Initially, usage will be near zero, but within a few days you'll see costs accumulating as users and repositories consume products beyond the allowance in their plan.
+### Troubleshooting budget conflicts
+
+Keep these limits in mind as you combine budgets across scopes:
+
+* **Budgets overlap, and the most restrictive one applies.** A user can be covered by an individual, cost center, organization, and enterprise budget at the same time. Whichever has the least headroom remaining blocks them first. If someone is blocked unexpectedly, review every scope that applies to them. For the full evaluation order, see [AUTOTITLE](/copilot/concepts/billing/budgets-for-usage-based-billing).
+* **You can't set different budgets for teams in the same cost center.** A budget applies to the whole cost center, not to teams within it. If two teams need separate budgets, create a separate cost center for each. Separate cost centers can still share the same Azure billing identity.
+* **Budgets don't add up across levels.** An enterprise budget isn't the sum of your cost center budgets, and raising one doesn't raise another. When you change a budget at one level, reconcile the totals at the others yourself.
+
## 4. Create a cost center with the REST API
Now that you understand how to create cost centers in the user interface, you can explore the REST API to see how cost centers can be created programmatically. Understanding the API helps you evaluate whether automation would benefit your organization.
diff --git a/content/copilot/concepts/billing/budgets-for-usage-based-billing.md b/content/copilot/concepts/billing/budgets-for-usage-based-billing.md
index d9679b8962f7..3863735c73ec 100644
--- a/content/copilot/concepts/billing/budgets-for-usage-based-billing.md
+++ b/content/copilot/concepts/billing/budgets-for-usage-based-billing.md
@@ -46,6 +46,12 @@ When a cost center's budget is exhausted, only users in that cost center are blo
> [!NOTE]
> A cost center budget is different from a cost center user-level budget. A cost center budget caps the team's **total metered charges** after the pool is exhausted. A cost center user-level budget caps **each member's individual consumption** across both the pool and metered phases, the same way other user-level budgets do. You can apply both to the same cost center.
+### Included usage controls for cost centers
+
+{% data reusables.billing.included-usage-controls %}
+
+Unlike a cost center budget, which caps metered charges only after the shared pool of {% data variables.product.prodname_ai_credits_short %} is exhausted, an included usage control limits how much of the pool a cost center can draw **before** the metered phase begins. To enable it, see [AUTOTITLE](/billing/how-tos/set-up-budgets).
+
### Organization budget
An organization budget caps metered charges for users who receive their {% data variables.product.prodname_copilot_short %} license through that organization. Like cost center budgets, it is only active after the shared pool is exhausted.
diff --git a/content/copilot/how-tos/copilot-sdk/features/hooks.md b/content/copilot/how-tos/copilot-sdk/features/hooks.md
index 1e15f414f7cc..a4b4a9e089c0 100644
--- a/content/copilot/how-tos/copilot-sdk/features/hooks.md
+++ b/content/copilot/how-tos/copilot-sdk/features/hooks.md
@@ -1028,16 +1028,16 @@ const session = await client.createSession({
For full type definitions, input/output field tables, and additional examples for every hook, see the API reference:
-- [AUTOTITLE](/copilot/how-tos/copilot-sdk/hooks/hooks-overview)
-- [AUTOTITLE](/copilot/how-tos/copilot-sdk/hooks/pre-tool-use)
-- [AUTOTITLE](/copilot/how-tos/copilot-sdk/hooks/post-tool-use)
-- [AUTOTITLE](/copilot/how-tos/copilot-sdk/hooks/user-prompt-submitted)
-- [AUTOTITLE](/copilot/how-tos/copilot-sdk/hooks/session-lifecycle)
-- [AUTOTITLE](/copilot/how-tos/copilot-sdk/hooks/error-handling)
+* [AUTOTITLE](/copilot/how-tos/copilot-sdk/hooks/hooks-overview)
+* [AUTOTITLE](/copilot/how-tos/copilot-sdk/hooks/pre-tool-use)
+* [AUTOTITLE](/copilot/how-tos/copilot-sdk/hooks/post-tool-use)
+* [AUTOTITLE](/copilot/how-tos/copilot-sdk/hooks/user-prompt-submitted)
+* [AUTOTITLE](/copilot/how-tos/copilot-sdk/hooks/session-lifecycle)
+* [AUTOTITLE](/copilot/how-tos/copilot-sdk/hooks/error-handling)
## See also
-- [AUTOTITLE](/copilot/how-tos/copilot-sdk/getting-started)
-- [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/custom-agents)
-- [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/streaming-events)
-- [AUTOTITLE](/copilot/how-tos/copilot-sdk/troubleshooting/debugging)
+* [AUTOTITLE](/copilot/how-tos/copilot-sdk/getting-started)
+* [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/custom-agents)
+* [AUTOTITLE](/copilot/how-tos/copilot-sdk/features/streaming-events)
+* [AUTOTITLE](/copilot/how-tos/copilot-sdk/troubleshooting/debugging)
diff --git a/content/copilot/how-tos/copilot-sdk/features/index.md b/content/copilot/how-tos/copilot-sdk/features/index.md
index 4e5d83814a03..cad1052de841 100644
--- a/content/copilot/how-tos/copilot-sdk/features/index.md
+++ b/content/copilot/how-tos/copilot-sdk/features/index.md
@@ -20,6 +20,7 @@ children:
- /mcp
- /plugin-directories
- /remote-sessions
+ - /session-limits
- /session-persistence
- /skills
- /steering-and-queueing
diff --git a/content/copilot/how-tos/copilot-sdk/features/session-limits.md b/content/copilot/how-tos/copilot-sdk/features/session-limits.md
new file mode 100644
index 000000000000..63e811411a8e
--- /dev/null
+++ b/content/copilot/how-tos/copilot-sdk/features/session-limits.md
@@ -0,0 +1,182 @@
+---
+title: Session limits
+shortTitle: Session limits
+intro: >-
+ Session limits let an application set an AI Credits budget for a Copilot
+ session. Use `sessionLimits` when creating or resuming a session to set a soft
+ cap for the current accounting window.
+versions:
+ fpt: '*'
+ ghec: '*'
+contentType: how-tos
+---
+
+
+
+
+## Configure a session limit
+
+Set `maxAiCredits` to the AI Credits soft cap for the session's current accounting window. Usage is checked after model calls return, so one response can exceed the configured value before the runtime blocks the next model call. The SDK forwards this value to the Copilot CLI when it creates or resumes the session.
+
+{% codetabs %}
+{% codetab typescript %}
+
+
+
+```typescript
+const session = await client.createSession({
+ onPermissionRequest: approveAll,
+ sessionLimits: {
+ maxAiCredits: 30,
+ },
+});
+
+const resumed = await client.resumeSession(session.sessionId, {
+ onPermissionRequest: approveAll,
+ sessionLimits: {
+ maxAiCredits: 30,
+ },
+});
+```
+
+{% endcodetab %}
+{% codetab python %}
+
+
+
+```python
+session = await client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ session_limits={
+ "max_ai_credits": 30,
+ },
+)
+
+resumed = await client.resume_session(
+ session.session_id,
+ on_permission_request=PermissionHandler.approve_all,
+ session_limits={
+ "max_ai_credits": 30,
+ },
+)
+```
+
+{% endcodetab %}
+{% codetab go %}
+
+
+
+```golang
+session, err := client.CreateSession(ctx, &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ SessionLimits: &rpc.SessionLimitsConfig{
+ MaxAiCredits: copilot.Float64(30),
+ },
+})
+
+resumed, err := client.ResumeSession(ctx, session.SessionID, &copilot.ResumeSessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ SessionLimits: &rpc.SessionLimitsConfig{
+ MaxAiCredits: copilot.Float64(30),
+ },
+})
+```
+
+{% endcodetab %}
+{% codetab dotnet %}
+
+
+
+```csharp
+var session = await client.CreateSessionAsync(new SessionConfig
+{
+ OnPermissionRequest = PermissionHandler.ApproveAll,
+ SessionLimits = new SessionLimitsConfig
+ {
+ MaxAiCredits = 30,
+ },
+});
+
+var resumed = await client.ResumeSessionAsync(session.SessionId, new ResumeSessionConfig
+{
+ OnPermissionRequest = PermissionHandler.ApproveAll,
+ SessionLimits = new SessionLimitsConfig
+ {
+ MaxAiCredits = 30,
+ },
+});
+```
+
+{% endcodetab %}
+{% codetab java %}
+
+
+
+```java
+CopilotSession session = client
+ .createSession(new SessionConfig()
+ .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
+ .setSessionLimits(new SessionLimitsConfig(30.0)))
+ .get();
+
+CopilotSession resumed = client
+ .resumeSession(session.getSessionId(), new ResumeSessionConfig()
+ .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
+ .setSessionLimits(new SessionLimitsConfig(30.0)))
+ .get();
+```
+
+{% endcodetab %}
+{% codetab rust %}
+
+
+
+```rust
+let limits = SessionLimitsConfig {
+ max_ai_credits: Some(30.0),
+};
+
+let session = client
+ .create_session(
+ SessionConfig::new()
+ .approve_all_permissions()
+ .with_session_limits(limits.clone()),
+ )
+ .await?;
+
+let resumed = client
+ .resume_session(
+ ResumeSessionConfig::new(session.id().clone())
+ .approve_all_permissions()
+ .with_session_limits(limits),
+ )
+ .await?;
+```
+
+{% endcodetab %}
+{% endcodetabs %}
+
+## Observe budget events
+
+Applications can subscribe to session events to update UI when the soft cap changes or the session reaches the exhausted-budget flow.
+
+| Event type | When it is emitted | Important fields |
+|---|---|---|
+| `session.session_limits_changed` | Active session limits changed. A `null` `sessionLimits` value means no limits are active. | `sessionLimits.maxAiCredits?` |
+| `session.usage_checkpoint` | The runtime records durable aggregate usage for resume and accounting. | `totalNanoAiu`, `totalPremiumRequests?` |
+| `session_limits_exhausted.requested` | The session reached the exhausted-budget flow and needs a user decision before continuing. | `requestId`, `maxAiCredits`, `usedAiCredits` |
+| `session_limits_exhausted.completed` | The exhausted-limit prompt was resolved. | `requestId`, `response.action`, `response.additionalAiCredits?`, `response.maxAiCredits?` |
+
+Use the generated event types for the SDK language you are using. For example, TypeScript narrows by `event.type`:
+
+```typescript
+session.on((event) => {
+ if (event.type === "session_limits_exhausted.requested") {
+ showBudgetDialog({
+ requestId: event.data.requestId,
+ maxAiCredits: event.data.maxAiCredits,
+ usedAiCredits: event.data.usedAiCredits,
+ });
+ }
+});
+```
diff --git a/content/copilot/how-tos/copilot-sdk/features/streaming-events.md b/content/copilot/how-tos/copilot-sdk/features/streaming-events.md
index e597af9e50f9..65d4cadd7411 100644
--- a/content/copilot/how-tos/copilot-sdk/features/streaming-events.md
+++ b/content/copilot/how-tos/copilot-sdk/features/streaming-events.md
@@ -443,6 +443,24 @@ Ephemeral. Context window utilization snapshot.
| `currentTokens` | `number` | ✅ | Current tokens in the context window |
| `messagesLength` | `number` | ✅ | Current message count in the conversation |
+### `session.session_limits_changed`
+
+Session limits changed for the current accounting window. A `null` `sessionLimits` value means no limits are active.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `sessionLimits` | `SessionLimitsConfig \| null` | ✅ | Current session limits, or `null` when no limits are active |
+| `sessionLimits.maxAiCredits` | `number` | | Maximum AI Credits allowed across the session's current accounting window |
+
+### `session.usage_checkpoint`
+
+Durable aggregate usage checkpoint used to reconstruct accounting when a session is resumed.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `totalNanoAiu` | `number` | ✅ | Session-wide accumulated nano-AI units cost at checkpoint time |
+| `totalPremiumRequests` | `number` | | Total number of premium API requests used at checkpoint time |
+
### `session.task_complete`
The agent has completed its assigned task.
@@ -692,6 +710,27 @@ Ephemeral. A queued command was resolved.
|------------|------|----------|-------------|
| `requestId` | `string` | ✅ | Matches the corresponding `command.queued` |
+### `session_limits_exhausted.requested`
+
+Ephemeral. The current session budget was exhausted and the runtime needs a user decision before continuing.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `requestId` | `string` | ✅ | Use this ID when responding to the pending exhausted-limit request |
+| `maxAiCredits` | `number` | ✅ | Configured max AI Credits for the current accounting window |
+| `usedAiCredits` | `number` | ✅ | AI Credits already consumed in the current accounting window |
+
+### `session_limits_exhausted.completed`
+
+Ephemeral. A pending exhausted-limit request was resolved.
+
+| Data Field | Type | Required | Description |
+|------------|------|----------|-------------|
+| `requestId` | `string` | ✅ | Matches the corresponding `session_limits_exhausted.requested` event |
+| `response.action` | `"add" \| "set" \| "unset" \| "cancel"` | ✅ | Action selected for the exhausted-limit request |
+| `response.additionalAiCredits` | `number` | | AI Credits to add to the current max when `response.action` is `"add"` |
+| `response.maxAiCredits` | `number` | | New absolute max AI Credits when `response.action` is `"set"` |
+
## Quick reference: agentic turn flow
A typical agentic turn emits events in this order:
@@ -744,6 +783,8 @@ session.idle → Ready for next message (ephemeral)
| `session.title_changed` | ✅ | Session | `title` |
| `session.context_changed` | | Session | `cwd`, `gitRoot?`, `repository?`, `branch?` |
| `session.usage_info` | ✅ | Session | `tokenLimit`, `currentTokens`, `messagesLength` |
+| `session.session_limits_changed` | | Session | `sessionLimits` |
+| `session.usage_checkpoint` | | Session | `totalNanoAiu`, `totalPremiumRequests?` |
| `session.task_complete` | | Session | `summary?` |
| `session.shutdown` | | Session | `shutdownType`, `codeChanges`, `modelMetrics` |
| `permission.requested` | ✅ | Permission | `requestId`, `permissionRequest` |
@@ -765,5 +806,7 @@ session.idle → Ready for next message (ephemeral)
| `external_tool.completed` | ✅ | External Tool | `requestId` |
| `command.queued` | ✅ | Command | `requestId`, `command` |
| `command.completed` | ✅ | Command | `requestId` |
+| `session_limits_exhausted.requested` | ✅ | Session | `requestId`, `maxAiCredits`, `usedAiCredits` |
+| `session_limits_exhausted.completed` | ✅ | Session | `requestId`, `response.action` |
| `exit_plan_mode.requested` | ✅ | Plan Mode | `requestId`, `summary`, `planContent`, `actions` |
| `exit_plan_mode.completed` | ✅ | Plan Mode | `requestId` |
diff --git a/content/copilot/how-tos/copilot-sdk/hooks/post-tool-use.md b/content/copilot/how-tos/copilot-sdk/hooks/post-tool-use.md
index 7fa71e2dcae3..c24e40c784d1 100644
--- a/content/copilot/how-tos/copilot-sdk/hooks/post-tool-use.md
+++ b/content/copilot/how-tos/copilot-sdk/hooks/post-tool-use.md
@@ -15,10 +15,10 @@ contentType: how-tos
-- Transform or filter tool results
-- Log tool execution for auditing
-- Add context based on results
-- Suppress results from the conversation
+* Transform or filter tool results
+* Log tool execution for auditing
+* Add context based on results
+* Suppress results from the conversation
> **Failure variant** — `onPostToolUse` only fires for successful tool executions. To observe **failed** tool calls, register `onPostToolUseFailure` (`on_post_tool_use_failure` in Python, `OnPostToolUseFailure` in Go/.NET, `on_post_tool_use_failure` in Rust). The handler receives `{ sessionId, toolName, toolArgs, error, timestamp, workingDirectory }` — the `error` field is a string extracted from the tool's failure result — and may return `{ additionalContext: string }` to inject extra guidance for the model (e.g. retry hints). See the [AUTOTITLE](/copilot/how-tos/copilot-sdk/hooks/hooks-overview) for the full list.
>
@@ -488,6 +488,6 @@ const session = await client.createSession({
## See also
-- [AUTOTITLE](/copilot/how-tos/copilot-sdk/hooks)
-- [AUTOTITLE](/copilot/how-tos/copilot-sdk/hooks/pre-tool-use)
-- [AUTOTITLE](/copilot/how-tos/copilot-sdk/hooks/error-handling)
+* [AUTOTITLE](/copilot/how-tos/copilot-sdk/hooks)
+* [AUTOTITLE](/copilot/how-tos/copilot-sdk/hooks/pre-tool-use)
+* [AUTOTITLE](/copilot/how-tos/copilot-sdk/hooks/error-handling)
diff --git a/content/copilot/tutorials/budgets/optimizing-your-budget-configuration.md b/content/copilot/tutorials/budgets/optimizing-your-budget-configuration.md
index 68a3024cad17..37b7d4480fad 100644
--- a/content/copilot/tutorials/budgets/optimizing-your-budget-configuration.md
+++ b/content/copilot/tutorials/budgets/optimizing-your-budget-configuration.md
@@ -22,6 +22,8 @@ Before optimizing your budget configuration, make sure you understand how budget
If you haven't set up budgets yet, start with [AUTOTITLE](/copilot/tutorials/budgets/getting-started-with-budget-controls) to get the basics in place, then come back to this guide to optimize your configuration.
+To decide between a simple setup and cost-center-based controls before you fine-tune, see the basic and advanced approaches in [AUTOTITLE](/billing/tutorials/control-costs-at-scale#choose-your-approach-basic-or-advanced-spend-controls).
+
## Sizing your budgets
The relationship between user-level budgets (ULB) and other budgets is the most common source of unexpected blocking. If user-level budgets collectively allow more consumption than the shared pool provides, the difference becomes metered charges, and your budgets need to be high enough to cover that gap.
@@ -34,6 +36,8 @@ Here's how to estimate:
If you also use cost center budgets, the sum of your cost center budgets and your enterprise budget should cover the gap. The enterprise budget applies to users not assigned to a cost center.
+If you want each cost center to stay within the {% data variables.product.prodname_ai_credits_short %} its own licenses fund, apply an included usage control to that cost center. This automatically caps the team's draw from the shared pool, so heavy use by one team doesn't consume another team's share before metered budgets apply. See [AUTOTITLE](/copilot/concepts/billing/budgets-for-usage-based-billing#included-usage-controls-for-cost-centers).
+
> [!TIP]
> Whenever you raise user-level budgets, re-check this calculation. Raising ULBs without raising the enterprise budget can cause the enterprise budget to block users before they reach their individual budgets.
@@ -81,6 +85,7 @@ This is the simplest configuration and a good starting point for most enterprise
**Configuration:**
* Create **cost centers** scoped to each organization. See [AUTOTITLE](/billing/how-tos/products/use-cost-centers).
+* Apply an **included usage control** to each cost center so a business unit can't draw more of the shared pool than the included {% data variables.product.prodname_ai_credits_short %} its own licenses fund, choosing whether to block or allow paid overage at the cap.
* Set a **cost center budget** for each business unit.
* Set an **enterprise budget** as a failsafe for any users not assigned to a cost center.
* Enable **"Stop usage when budget limit is reached"** on all budgets.
diff --git a/data/reusables/billing/included-usage-controls.md b/data/reusables/billing/included-usage-controls.md
new file mode 100644
index 000000000000..7d40bd2814b5
--- /dev/null
+++ b/data/reusables/billing/included-usage-controls.md
@@ -0,0 +1 @@
+Included usage controls cap a cost center's included usage to the amount of {% data variables.product.prodname_ai_credits_short %} funded by the licenses assigned to that cost center. {% data variables.product.github %} sets this cap automatically and adjusts it as licensed members are added or removed, you don't enter an amount. When a cost center reaches its cap, you choose whether its members are blocked or their additional usage continues as paid overage.
diff --git a/src/article-api/lib/summarize-schema.ts b/src/article-api/lib/summarize-schema.ts
index 68a5c15360e0..8ae39784399b 100644
--- a/src/article-api/lib/summarize-schema.ts
+++ b/src/article-api/lib/summarize-schema.ts
@@ -60,11 +60,29 @@ function renderTypeConstraints(schema: JsonSchema): string {
return parts.join(', ') || 'object'
}
+// When a titled object type has already been fully expanded once in a schema,
+// later occurrences are rendered as a short "(see above)" reference instead of
+// being re-expanded. Large REST response schemas reuse shared types (e.g.
+// `Simple User` recurs dozens of times), so this keeps output compact and fast
+// to render. Returns true if the caller should emit a reference; marks the
+// title as seen when it is about to be expanded for the first time.
+function shouldReference(
+ title: string | undefined,
+ canExpand: boolean,
+ seen: Set,
+): boolean {
+ if (!canExpand || !title) return false
+ if (seen.has(title)) return true
+ seen.add(title)
+ return false
+}
+
function renderCompositionVariants(
keyword: string,
variants: JsonSchema[],
indent: number,
depth: number,
+ seen: Set,
): string {
const prefix = ' '.repeat(indent)
const label = keyword.replace('Of', ' of')
@@ -72,9 +90,14 @@ function renderCompositionVariants(
for (const variant of variants) {
const name = variant.title || renderTypeConstraints(variant)
+ const canExpand = depth < MAX_DEPTH && Boolean(variant.properties)
+ if (shouldReference(variant.title, canExpand, seen)) {
+ lines.push(`${prefix} * **${name}** (see above)`)
+ continue
+ }
lines.push(`${prefix} * **${name}**`)
- if (depth < MAX_DEPTH && variant.properties) {
- const nested = renderProperties(variant, indent + 2, depth + 1)
+ if (canExpand) {
+ const nested = renderProperties(variant, indent + 2, depth + 1, seen)
if (nested) lines.push(nested)
}
}
@@ -86,6 +109,7 @@ function renderProperties(
schema: JsonSchema,
indent: number,
depth: number,
+ seen: Set,
requiredFields?: string[],
): string {
const props = schema.properties || {}
@@ -103,9 +127,14 @@ function renderProperties(
lines.push(`${prefix}* \`${name}\`: ${reqStr}${label}:`)
for (const variant of prop[compositionKey]!) {
const vName = variant.title || renderTypeConstraints(variant)
+ const canExpand = depth < MAX_DEPTH && Boolean(variant.properties)
+ if (shouldReference(variant.title, canExpand, seen)) {
+ lines.push(`${prefix} * **${vName}** (see above)`)
+ continue
+ }
lines.push(`${prefix} * **${vName}**`)
- if (depth < MAX_DEPTH && variant.properties) {
- const nested = renderProperties(variant, indent + 2, depth + 1)
+ if (canExpand) {
+ const nested = renderProperties(variant, indent + 2, depth + 1, seen)
if (nested) lines.push(nested)
}
}
@@ -118,12 +147,17 @@ function renderProperties(
if (propType === 'array' && prop.items) {
const itemTitle = prop.items.title
- if (prop.items.properties && depth < MAX_DEPTH) {
+ const canExpand = Boolean(prop.items.properties) && depth < MAX_DEPTH
+ if (shouldReference(itemTitle, canExpand, seen)) {
+ lines.push(
+ `${prefix}* \`${name}\`: ${reqStr}array of \`${itemTitle}\`${isNullable ? ' or null' : ''} (see above)`,
+ )
+ } else if (canExpand) {
const label = itemTitle
? `array of \`${itemTitle}\`${isNullable ? ' or null' : ''}`
: `array of objects${isNullable ? ' or null' : ''}`
lines.push(`${prefix}* \`${name}\`: ${reqStr}${label}:`)
- lines.push(renderProperties(prop.items, indent + 1, depth + 1))
+ lines.push(renderProperties(prop.items, indent + 1, depth + 1, seen))
} else {
lines.push(
`${prefix}* \`${name}\`: ${reqStr}array of ${renderTypeConstraints(prop.items)}${isNullable ? ' or null' : ''}`,
@@ -132,8 +166,12 @@ function renderProperties(
} else if (prop.properties && depth < MAX_DEPTH) {
// renderTypeConstraints handles string[] types (e.g. ["object","null"] → "object or null")
const label = prop.title ? `\`${prop.title}\`` : renderTypeConstraints(prop)
- lines.push(`${prefix}* \`${name}\`: ${reqStr}${label}:`)
- lines.push(renderProperties(prop, indent + 1, depth + 1))
+ if (shouldReference(prop.title, true, seen)) {
+ lines.push(`${prefix}* \`${name}\`: ${reqStr}${label} (see above)`)
+ } else {
+ lines.push(`${prefix}* \`${name}\`: ${reqStr}${label}:`)
+ lines.push(renderProperties(prop, indent + 1, depth + 1, seen))
+ }
} else {
// renderTypeConstraints handles string[] types (e.g. ["string","null"] → "string or null")
lines.push(`${prefix}* \`${name}\`: ${reqStr}${renderTypeConstraints(prop)}`)
@@ -151,10 +189,15 @@ function renderProperties(
export function summarizeSchema(schema: JsonSchema): string {
if (!schema || typeof schema !== 'object') return ''
+ // Tracks titled object types already expanded once in this schema. Later
+ // occurrences are rendered as a "(see above)" reference to keep output
+ // compact and fast to render (shared types can recur dozens of times).
+ const seen = new Set()
+
// Handle top-level composition
for (const keyword of ['oneOf', 'anyOf', 'allOf'] as const) {
if (schema[keyword]) {
- return renderCompositionVariants(keyword, schema[keyword]!, 0, 0)
+ return renderCompositionVariants(keyword, schema[keyword]!, 0, 0, seen)
}
}
@@ -181,9 +224,14 @@ export function summarizeSchema(schema: JsonSchema): string {
const lines = [`Array${constraintStr} of ${titlePart}objects${nullSuffix}: ${label}:`]
for (const variant of items[compositionKey]!) {
const name = variant.title || renderTypeConstraints(variant)
+ const canExpand = Boolean(variant.properties)
+ if (shouldReference(variant.title, canExpand, seen)) {
+ lines.push(` * **${name}** (see above)`)
+ continue
+ }
lines.push(` * **${name}**`)
- if (variant.properties) {
- const nested = renderProperties(variant, 2, 1)
+ if (canExpand) {
+ const nested = renderProperties(variant, 2, 1, seen)
if (nested) lines.push(nested)
}
}
@@ -193,7 +241,8 @@ export function summarizeSchema(schema: JsonSchema): string {
if (items.properties) {
const label = itemTitle ? `\`${itemTitle}\`` : 'objects'
const nullSuffix = isNullable ? ' or null' : ''
- return `Array${constraintStr} of ${label}${nullSuffix}:\n${renderProperties(items, 1, 1)}`
+ if (itemTitle) seen.add(itemTitle)
+ return `Array${constraintStr} of ${label}${nullSuffix}:\n${renderProperties(items, 1, 1, seen)}`
}
return `Array${constraintStr} of ${renderTypeConstraints(items)}${isNullable ? ' or null' : ''}`
@@ -201,7 +250,12 @@ export function summarizeSchema(schema: JsonSchema): string {
// Handle top-level object
if (schema.properties) {
- return renderProperties(schema, 0, 0)
+ // Note: we deliberately do NOT pre-mark schema.title here. Unlike the
+ // array-items case above, a top-level object emits no visible titled
+ // header, so pre-marking would make a self-referential property render a
+ // dangling "(see above)" pointing at nothing. Letting it expand one
+ // depth-bounded level is correct and clearer.
+ return renderProperties(schema, 0, 0, seen)
}
return renderTypeConstraints(schema)
diff --git a/src/article-api/tests/summarize-schema.test.ts b/src/article-api/tests/summarize-schema.test.ts
index c468db5ea466..36dacf578afd 100644
--- a/src/article-api/tests/summarize-schema.test.ts
+++ b/src/article-api/tests/summarize-schema.test.ts
@@ -197,3 +197,94 @@ describe('summarizeSchema — OAS 3.1 nullable handling', () => {
expect(summarizeSchema('not an object')).toBe('')
})
})
+
+describe('summarizeSchema — repeated titled type deduplication', () => {
+ type Schema = Parameters[0]
+ const user: Schema = {
+ type: 'object',
+ title: 'Simple User',
+ properties: {
+ login: { type: 'string' },
+ id: { type: 'integer' },
+ },
+ }
+
+ it('expands a titled type once then references it with "(see above)"', () => {
+ const schema: Schema = {
+ type: 'object',
+ properties: {
+ actor: user,
+ assignee: user,
+ assigner: user,
+ },
+ }
+ const result = summarizeSchema(schema)
+ // Expanded exactly once
+ expect(result.match(/`login`/g)?.length).toBe(1)
+ // Two later references
+ expect(result.match(/\(see above\)/g)?.length).toBe(2)
+ expect(result).toContain('`assignee`: `Simple User` (see above)')
+ })
+
+ it('references repeated titled variants inside a composition', () => {
+ const schema: Schema = {
+ type: 'array',
+ items: {
+ anyOf: [
+ {
+ type: 'object',
+ title: 'Event A',
+ properties: { actor: user, target: user },
+ },
+ {
+ type: 'object',
+ title: 'Event B',
+ properties: { actor: user },
+ },
+ ],
+ },
+ }
+ const result = summarizeSchema(schema)
+ // Simple User expanded once across the whole schema
+ expect(result.match(/`login`/g)?.length).toBe(1)
+ expect((result.match(/\(see above\)/g) || []).length).toBeGreaterThanOrEqual(2)
+ })
+
+ it('does not reference untitled (anonymous) objects', () => {
+ const anon: Schema = {
+ type: 'object',
+ properties: { a: { type: 'string' } },
+ }
+ const schema: Schema = {
+ type: 'object',
+ properties: { first: anon, second: anon },
+ }
+ const result = summarizeSchema(schema)
+ expect(result).not.toContain('(see above)')
+ expect(result.match(/`a`/g)?.length).toBe(2)
+ })
+
+ it('expands a self-referential top-level object before referencing it (no dangling ref)', () => {
+ // A top-level object emits no visible titled header. With the top-level
+ // title NOT pre-marked, the first occurrence of the recursive property
+ // expands visibly, so any deeper "(see above)" points to that expansion
+ // rather than a header that was never rendered.
+ const node: Schema = {
+ type: 'object',
+ title: 'Category',
+ properties: {
+ name: { type: 'string' },
+ },
+ }
+ node.properties!.parent = node
+ const result = summarizeSchema(node)
+ // `Category` is expanded at least once (its `name` field is visible)
+ // before the recursive reference appears.
+ const firstExpansion = result.indexOf('`name`')
+ const firstReference = result.indexOf('(see above)')
+ expect(firstExpansion).toBeGreaterThanOrEqual(0)
+ if (firstReference >= 0) {
+ expect(firstExpansion).toBeLessThan(firstReference)
+ }
+ })
+})