Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ Run these directly in Copilot Chat (CLI or VS Code):
**Notes:**

* If prompted for a build mode, choose remote or local in chat.
* If referenced agents declare discoveryExtensions.knowledgeBases, provide the requested knowledgeBaseId values in /bookshelves/{bookshelf}/knowledgeBases/{knowledgebase}/versions/{version} format. Starter-kit deployment does not create knowledge bases; it patches user-provided IDs into each deployed agent.
* Both deployers default new configurations to Discovery GA `2026-06-01`; existing local configs and saved runs retain explicit API versions.
* Both deployers set agent `humanInTheLoop` and all bound tool `confirmation` values to `Disabled` in deployed agents, overriding source settings for all future use. They do not bypass nodepool capacity confirmation or unexpected platform approvals.
* Agent-deployer validation names its conversation after the investigation so Studio can select its history and associated outputs. Starter-kit deployment does not create validation conversations.
* If referenced agents declare discoveryExtensions.knowledgeBases, provide the requested knowledgeBaseId values in `/bookshelves/{bookshelf}/knowledgeBases/{knowledgebase}` format for GA. Explicit preview configs also accept `/versions/{version}`; GA rejects that suffix. Starter-kit deployment does not create knowledge bases; it patches user-provided IDs into each deployed agent.
* If prompted for Supercomputer nodepool confirmation, choose Proceed / Stop in chat before tool builds continue.
* Starter-kit deployment deploys each referenced agent individually and does not create a validation investigation; the summary lists deployed agents, deployed tools, and sample prompts you can use to test the deployment.
5 changes: 4 additions & 1 deletion .github/skills/discovery-services-agent-deployer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ allowed-tools: shell

Runs on Windows, macOS, and Linux through PowerShell 7+ (`pwsh`). Requires `git`, `python` with `pip`, and Azure CLI (`az`). Docker is optional because the runner can use Azure Container Registry Tasks when local Docker is unavailable.

New configurations default to Discovery GA `2026-06-01`. Explicit preview versions in local configs and saved runs remain unchanged. See [configuration](references/configuration.md) for version-specific validation routes and knowledge-base binding formats.

## What this skill does

Deploy one or more Discovery catalog agents from this repository with a single deterministic runner:
Expand All @@ -20,6 +22,8 @@ pwsh -NoProfile -ExecutionPolicy Bypass -File .github/skills/discovery-services-

The single PowerShell script discovers the agent folder, selects its tool folder when present, builds and pushes the image, creates or updates the `Microsoft.Discovery/tools` resource, patches a run-local copy of `agent.yaml`, deploys the agent, validates it with a temporary investigation, and prints a final summary. If the agent has no tool, it skips `build` and `deploy-tool` and continues with agent deployment. It persists stage status to `stage-todos.json` in the run directory so the invoking assistant can keep native Copilot task tracking in sync without duplicating progress logs.

Agent deployment sets `humanInTheLoop` and every bound tool's `confirmation` to `Disabled`, even if the source YAML enables them. This persists for subsequent uses of the deployed agent, not just validation. The source YAML is unchanged. This does not bypass Supercomputer nodepool confirmation, platform-required actions, or approval requests in agent instructions; unexpected validation approvals still require user input.

## When to use

Use this skill for requests like:
Expand Down Expand Up @@ -171,4 +175,3 @@ If the runner fails, report the failed stage, the action required from the scrip
- `references/troubleshooting.md` - common failures and recovery steps

Keep `SKILL.md` lean. Prefer the single public runner and references over ad-hoc scripts or long manual instructions. The deployer implementation is intentionally consolidated into `scripts/deploy-discovery-agent.ps1` so there is only one executable script to maintain.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"acrName": "<your-acr-name-without-azurecr-io>",
"acrResourceGroup": "<optional-acr-resource-group-when-different-from-resourceGroup>",
"location": "uksouth",
"apiVersion": "2026-02-01-preview",
"apiVersion": "2026-06-01",
"workspaceEndpoint": "https://<your-workspace>.workspace.discovery.azure.com",
"project": "<your-project-name>",
"tenantId": "<your-tenant-id-guid>",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Copy-Item .github/skills/discovery-services-agent-deployer/config.template.json
| `acrName` | Yes | ACR name without `.azurecr.io`. |
| `acrResourceGroup` | No | ACR resource group when the registry is not in `resourceGroup`; defaults to `resourceGroup`. |
| `location` | Yes | Tool resource region. Ask the user for this value when it is missing; do not infer `uksouth` from the example. |
| `apiVersion` | No | Defaults to `2026-02-01-preview` from `config.template.json`. |
| `apiVersion` | No | Defaults to GA `2026-06-01` from `config.template.json`. Explicit preview overrides are preserved. |
| `workspaceEndpoint` | Yes | Discovery workspace endpoint. |
| `project` | Yes | Discovery project name. |
| `tenantId` | Yes | Entra tenant for Discovery auth. |
Expand All @@ -30,6 +30,10 @@ Copy-Item .github/skills/discovery-services-agent-deployer/config.template.json

## Copilot-style input behavior

The configured API version applies to ARM tools and workspace agent, investigation, and conversation requests. Validation uses the GA `/conversations/{name}/openai/v1/responses` route without an `api-version` query and sends `agent_reference`. An explicit `2026-02-01-preview` override instead uses `/conversations/{name}/openai/responses?api-version=2026-02-01-preview` and the preview `agent` field, including when polling or resuming validation.

Existing ignored config files and saved run configs are not automatically migrated. To opt an existing environment into GA, set `apiVersion` to `2026-06-01` before starting a new run. For GA agent bindings, use knowledge-base IDs in `/bookshelves/{bookshelf}/knowledgeBases/{knowledgebase}` format, without `/versions/{version}`. Preview also accepts versioned IDs. Do not silently strip a version from a user-provided ID.

The runner never assumes `config.json` is present and never uses terminal `Read-Host` prompts for required config. Stage 1 verifies these required values are present: `subscriptionId`, `resourceGroup`, `acrName`, `location`, `workspaceEndpoint`, `project`, `tenantId`, `chatModel`, and `forceToolImageRebuild`. If `config.json` is absent or any required value is missing, Stage 1 prints a structured block:

```text
Expand All @@ -49,4 +53,3 @@ After Stage 1 succeeds, the runner writes the resolved values into the run-local
`buildMode` and `confirmSupercomputerNodepools` are intentionally not config fields. If Docker is available, the runner asks for build mode as a per-run Copilot choice and should be rerun with `-BuildMode remote` or `-BuildMode local`. Before building, the runner prints the tool image and recommended SKU choices; ask the customer to Proceed or Stop based on Supercomputer nodepool capacity. If they choose Proceed, rerun build with `-ConfirmSupercomputerNodepools`. Do not infer these choices and do not write them to `config.json`. If Docker is unavailable, the runner uses remote ACR build.

Validation prompts are generated at runtime by default. If `testPrompt` is present and non-empty in `config.json`, it acts as a customer-provided override and validation uses it directly. Otherwise, for tool-backed agents, GitHub Copilot should generate a short, concrete validation prompt from the deployed agent/tool context at validation time and pass it with `-ValidationPrompt`.

Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ The runner is a single self-contained PowerShell script and owns:
Tool deployment resolves `{name}.azurecr.io/` and `{name}/` image registry
placeholders without changing Handlebars action inputs such as `{{name}}`.

The run-local agent YAML overrides `discoveryExtensions.humanInTheLoop` and every
bound tool's `confirmation` to `Disabled`, including explicit `Enabled` source
settings. The upsert persists these settings for all subsequent uses of the
deployed agent; the source YAML is not changed. This does not disable unrelated
platform approvals or prevent instructions from asking for confirmation.

Only `scripts/deploy-discovery-agent.ps1` is used for deployment logic. Keep new stage behavior in that script rather than adding separate internal stage scripts, so this skill stays aligned with the single-script `discovery-services-starter-kit-deployer` layout.

## Stage-at-a-time execution
Expand Down Expand Up @@ -62,6 +68,12 @@ records the conversation, response, investigation, original prompt, verdict,
and diagnostic reason. Failed and timed-out investigations are retained even
when successful-test cleanup is enabled.

New validation conversations use the investigation's name as their display
name, matching Studio's primary-conversation selection for session history and
output assets. This applies to initial and retry investigations. Existing
conversations are not renamed or merged; already-created sessions may still
show an empty primary conversation.

For tool-backed agents, success requires a correlated call and successful
output from the deployed tool, not just an assistant message. Code environments
must additionally print this receipt in tool stdout after executing assertions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ function Load-BuilderConfig {
Write-Host "[config] Location '$Location' is outside the preferred list (eastus, uksouth, swedencentral). Continuing with custom value."
}
}
$ApiVersion = Resolve-ConfigField -ConfigObj $WorkingConfig -Field 'apiVersion' -Default '2026-02-01-preview' -PromptLabel 'apiVersion'
$ApiVersion = Resolve-ConfigField -ConfigObj $WorkingConfig -Field 'apiVersion' -Default '2026-06-01' -PromptLabel 'apiVersion'
$WorkspaceEndpoint = Resolve-ConfigField -ConfigObj $WorkingConfig -Field 'workspaceEndpoint' -Required:($Required.ContainsKey('workspaceEndpoint')) -PromptLabel 'workspaceEndpoint (https://<workspace>.workspace.discovery.azure.com)'
$Project = Resolve-ConfigField -ConfigObj $WorkingConfig -Field 'project' -Required:($Required.ContainsKey('project')) -PromptLabel 'project'
$TenantId = Resolve-ConfigField -ConfigObj $WorkingConfig -Field 'tenantId' -Required:($Required.ContainsKey('tenantId')) -PromptLabel 'tenantId'
Expand Down Expand Up @@ -1317,19 +1317,24 @@ if isinstance(model, dict):
model['id'] = chat_model
doc['model'] = model

if tool_id:
disc = doc.get('discoveryExtensions') or {}
tools = disc.get('tools') or []
if not isinstance(tools, list):
tools = []
disc = doc.get('discoveryExtensions') or {}
if not isinstance(disc, dict):
raise SystemExit('Invalid discoveryExtensions: expected a mapping')
disc['humanInTheLoop'] = 'Disabled'
tools = disc.get('tools') or []
if not isinstance(tools, list):
raise SystemExit('Invalid discoveryExtensions.tools: expected a list')
for item in tools:
if not isinstance(item, dict):
raise SystemExit('Invalid discoveryExtensions.tools entry: expected a mapping')
item['confirmation'] = 'Disabled'

if tool_id:
tool_leaf = tool_id.rsplit('/', 1)[-1].lower()
replaced_any = False
deduped = []
seen_leaves = set()
for item in tools:
if not isinstance(item, dict):
continue
current = str(item.get('toolId', '')).strip()
current_leaf = current.rsplit('/', 1)[-1].lower() if current else ''
is_placeholder = current.startswith('{{') and current.endswith('}}')
Expand All @@ -1339,8 +1344,6 @@ if tool_id:
if tool_leaf in seen_leaves:
continue
item['toolId'] = tool_id
if 'confirmation' not in item:
item['confirmation'] = 'Disabled'
deduped.append(item)
seen_leaves.add(tool_leaf)
replaced_any = True
Expand All @@ -1355,7 +1358,7 @@ if tool_id:
deduped.append({'toolId': tool_id, 'confirmation': 'Disabled'})

disc['tools'] = deduped
doc['discoveryExtensions'] = disc
doc['discoveryExtensions'] = disc

with open(path, 'w', encoding='utf-8') as f:
yaml.safe_dump(doc, f, sort_keys=False, allow_unicode=False)
Expand All @@ -1368,6 +1371,7 @@ $PatchArgs = @($PatchPy, $TempAgentYaml, $Config.chatModel)
if ($HasTool) { $PatchArgs += $State.deploy.toolResourceId }
python @PatchArgs
if ($LASTEXITCODE -ne 0) { throw 'Failed to patch temp agent.yaml' }
Write-Host '[stage-04] Agent human-in-the-loop and tool confirmations are disabled for the deployed agent.'

$CurrentTenant = (az account show --query tenantId -o tsv --only-show-errors).Trim()
if ($CurrentTenant -ne $Config.tenantId) {
Expand Down Expand Up @@ -1861,6 +1865,15 @@ def call_with_retries(method, url, **kwargs):
base = cfg["workspaceEndpoint"].rstrip("/")
api = cfg["apiVersion"]
project = cfg["project"]
is_ga_api = api >= "2026-06-01"

def response_url(conversation_name, response_id=None):
route = "openai/v1/responses" if is_ga_api else "openai/responses"
url = f"{base}/conversations/{conversation_name}/{route}"
if response_id:
url += f"/{response_id}"
return url if is_ga_api else f"{url}?api-version={api}"

safe = re.sub(r"[^a-z0-9-]", "-", agent_name.lower())[:8].strip("-") or "agent"

# Use deterministic investigation naming (timestamp-based, not random)
Expand All @@ -1883,7 +1896,7 @@ else:
create = call_with_retries("PUT", inv_url, headers=headers(t), json={"description": "Post-deploy validation", "displayName": f"Validation Test {agent_name}"})
create.raise_for_status()
conv = call_with_retries("POST", f"{base}/conversations?api-version={api}", headers=headers(t), json={
"displayName": f"validation-{agent_name}",
"displayName": inv,
"investigationName": f"/projects/{project}/investigations/{inv}",
"projectName": project,
})
Expand Down Expand Up @@ -1978,11 +1991,11 @@ def assess_response(body):
def wait_for_completion(conversation_name, text_prompt, wait_seconds):
t_local = token()
if resume_response:
resp = call_with_retries("GET", f"{base}/conversations/{conversation_name}/openai/v1/responses/{resume_response}", headers=headers(t_local))
resp = call_with_retries("GET", response_url(conversation_name, resume_response), headers=headers(t_local))
else:
resp = call_with_retries("POST", f"{base}/conversations/{conversation_name}/openai/v1/responses", headers=headers(t_local), json={
resp = call_with_retries("POST", response_url(conversation_name), headers=headers(t_local), json={
"input": [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": text_prompt}]}],
"agent": {"type": "agent_reference", "name": agent_name},
("agent_reference" if is_ga_api else "agent"): {"type": "agent_reference", "name": agent_name},
})
resp.raise_for_status()
body_local = resp.json()
Expand All @@ -2009,7 +2022,7 @@ def wait_for_completion(conversation_name, text_prompt, wait_seconds):
elapsed = int(time.time() - start_time)
print(f"[validation] Polling attempt {poll_count}, elapsed {elapsed}s, status={status_local}...", flush=True)
t_local = token()
poll = call_with_retries("GET", f"{base}/conversations/{conversation_name}/openai/v1/responses/{rid_local}", headers=headers(t_local))
poll = call_with_retries("GET", response_url(conversation_name, rid_local), headers=headers(t_local))
poll.raise_for_status()
body_local = poll.json()
status_local = body_local.get("status", "")
Expand Down Expand Up @@ -2092,7 +2105,7 @@ for attempt in range(1, max_retries + 1):
_created_inv_urls.append(inv_url)
# Create new conversation with new investigation
conv = call_with_retries("POST", f"{base}/conversations?api-version={api}", headers=headers(t), json={
"displayName": f"validation-{agent_name}",
"displayName": inv,
"investigationName": f"/projects/{project}/investigations/{inv}",
"projectName": project,
})
Expand Down Expand Up @@ -2126,7 +2139,7 @@ for attempt in range(1, max_retries + 1):
_created_inv_urls.append(inv_url)
# Create new conversation with new investigation
conv = call_with_retries("POST", f"{base}/conversations?api-version={api}", headers=headers(t), json={
"displayName": f"validation-{agent_name}",
"displayName": inv,
"investigationName": f"/projects/{project}/investigations/{inv}",
"projectName": project,
})
Expand Down
Loading
Loading