Commit b429308
authored
feat(routines): add azd ai routine commands (#8241)
* feat(routines): implement azd ai routine commands
Add the full v1 routine command subtree to the azure.ai.routines
extension as specified in the design spec (PR #8200).
Commands implemented:
- routine create, update, show, list, delete
- routine enable, disable (dedicated idempotent action routes)
- routine dispatch (calls dispatch_async, --async flag for client-side wait)
- routine run list (auto-paging, --top, --filter)
New packages:
- internal/exterrors/ -- structured error codes and helpers
- internal/pkg/routines/ -- data-plane HTTP client and models
- internal/cmd/endpoint.go -- 5-level project endpoint resolver
Wire format: trigger/action as Record with 'default' key.
All calls include x-ms-foundry-features-opt-in: Routines=V1Preview header.
Also adds the design spec at cli/azd/docs/design/ai-routine-design-spec.md.
* test(routines): add unit tests for endpoint, create, manifest, and models
- Add readAzdProjectSourcesFunc seam to endpoint.go for daemon isolation
- endpoint_test.go: isFoundryHost, validateProjectEndpoint, full cascade tests
- routine_create_test.go: buildTrigger and buildAction table tests
- routine_manifest_test.go: readRoutineManifest (JSON/YAML), mergeRoutineFromFile, applyUpdateFlags, getTrigger/getAction
- models_test.go: TriggerCLIToWire and ActionCLIToWire completeness
- Add yaml struct tags to models.go for YAML manifest support
* test(routines): align test patterns with azure.ai.agents extension
- Extract stubAzdProjectSources() helper (mirrors stubAzdHostedSources in agents)
- isolateFromAzdDaemon now also clears AZD_SERVER env var
- Add t.Parallel() to all pure-function tests (isFoundryHost, validateProjectEndpoint,
buildTrigger, buildAction, mergeRoutineFromFile, applyUpdateFlags, getTrigger/getAction,
TriggerCLIToWire/ActionCLIToWire map checks)
* fix(routines): address CI lint and spell-check failures
- cspell.yaml: add exterrors, sess, routineName, azdProjectSources to word list
- endpoint.go: remove unused projectEndpointPathPrefix constant
- routine_create.go: wrap long buildAction() call (line >125 chars)
- routine_update.go: wrap long --file flag help text
- routine_manifest_test.go: expand inline map literals to multi-line
- client.go: wrap ListRoutineRuns signature to fit 125-char limit
- Run gofmt -w and go fix on all files (codes.go, client.go, models.go formatting)
* fix(routines): remove unused ptrBool helper
golangci-lint flagged ptrBool as unused. The function had no call sites; the //go:fix inline directive does not exempt it from the unused linter.
* docs(routines): remove design spec from PR
The design spec is tracked separately in PR #8200; this PR focuses on the implementation only.
* fix(routines): close response bodies per page and preserve filter on pagination
- Extract getPage helper so resp.Body.Close runs per iteration in
ListRoutines and ListRoutineRuns (defer-in-loop leaked FDs).
- Preserve the filter query param across pages in ListRoutineRuns;
previously page 2+ only carried pageToken and dropped the filter.
- Correct dispatch command help text: the service always runs routines
asynchronously, so the old 'waits and streams' wording was wrong.
* fix(routines): flatten command tree to remove duplicate 'routine routine'
The extension namespace 'ai.routine' already mounts the extension under
'azd ai routine'. Adding a 'routine' subcommand group on top of that
produced the redundant 'azd ai routine routine <cmd>' path.
Move --project-endpoint persistent flag and all subcommands directly onto
rootCmd so the correct usage is 'azd ai routine <cmd>'.
* fix(routines): align data-plane client with Foundry Routines TypeSpec
The first cut of the Routines client used header/route/field shapes that
did not match the TypeSpec being merged in azure-rest-api-specs#42779.
Realign the extension with the spec so requests round-trip cleanly:
* Preview header renamed from 'x-ms-foundry-features-opt-in' to
'Foundry-Features' (the value 'Routines=V1Preview' was already correct).
* Async dispatch route renamed ':dispatch_async' -> ':dispatchAsync';
the action segment is case-sensitive per spec.
* Dropped the non-existent ':enable' / ':disable' action routes;
enable/disable now read the routine and PUT it back with 'enabled'
flipped (idempotent: no-op if already at the target value).
* DispatchRoutineRequest wraps a discriminated 'payload' object whose
'type' must match the routine's action type; --conversation-id was
removed from dispatch (the spec does not expose it).
* Routine.Action is now a single discriminated object (not an 'actions'
map keyed by name).
* RoutineAction.AgentName -> AgentID; the CLI flag is renamed to
--agent-id accordingly.
* RoutineTrigger.Cron -> CronExpression to match the TypeSpec field.
* PagedRoutine pagination follows the absolute 'nextLink' URL from
Azure.Core.Page<Routine> instead of re-deriving a continuation query.
* RoutineRun gains the additional fields documented in the spec
(phase, trigger_type, attempt_source, action_type, triggered_at,
dispatch_id, action_correlation_id, response_id, error_type,
error_message); 'run list' now prints phase alongside status.
* EventRoutineTrigger fields aligned to the spec: connection_id, owner,
repository, actions[]; removed 'assignee'.
* DispatchRoutineResponse drops the unused 'status' field.
Tests, mock manifests, and the E2E driver were updated to the new
contract (--agent-id, agent_id, cron_expression, single 'action').
Note: the live Foundry Routines preview endpoint still returns HTTP 500
on /routines?api-version=v1 even with the correct request shape; that is
an upstream service bug tracked separately.
* fix(routines): clarify comment for github_issue fields in RoutineTrigger
* fix(routines): address PR review feedback
- endpoint: reject project endpoints with an explicit port so the
normalized URL cannot silently strip a non-default port
- routine create: only set Enabled from --enabled when the user
explicitly passes the flag, so a manifest's enabled value is
honored; default to enabled=true if neither source provides one
- routine create: explicitly reject --trigger github-issue (deferred
for v1) instead of producing an incomplete github_issue trigger
- routine_helpers: boolStr now returns "unknown" for a nil pointer
to avoid displaying "true" when the field is absent from the
service response
- routine_manifest: surface applyUpdateFlags user-input errors as
exterrors.Validation (CodeInvalidParameter) for consistent CLI
error shapes
* chore(routines): add .golangci.yaml and AGENTS.md to align with sibling extensions
Other AI extensions (projects, agents, toolboxes, inspector) ship a
.golangci.yaml lint config and an AGENTS.md contributor guide. Add both
to azure.ai.routines so it follows the same convention, and register
the project-specific �xterrors word in cspell.yaml.
* fix(routines): use camelCase JSON tags to match Foundry service wire
The deployed Foundry Routines data plane applies a camelCase property
naming policy on the wire (e.g. `cronExpression`, `timeZone`,
`agentId`), even though the upstream TypeSpec / OpenAPI document still
emits snake_case. With snake_case JSON tags, `routine create` and
`update` always failed with errors like:
triggers['default'].cronExpression must be provided for schedule routines
exactly one of action.agentId or action.agentEndpointId must be provided
and routines read back from `show` / `list` would have empty
trigger/action fields because the camelCase wire payload did not
deserialize into snake_case-tagged Go fields.
Switch the JSON tags on `Routine`, `RoutineTrigger`, `RoutineAction`,
`RoutineRun`, `PagedRoutineRun`, and `DispatchRoutineResponse` to
camelCase so requests/responses round-trip cleanly against the deployed
service. YAML tags stay snake_case so user-facing `--file` manifests
keep the documented convention.
Verified against a live project endpoint: create/list/show now reach
the service correctly (residual `InternalServerError` from the
backend is unrelated and reproduces from raw curl with the same body).
* feat(routines): align with spec PR #43186 and fix HTTP/2 hang
Use azure-rest-api-specs PR #43186 (Foundry Routines TypeSpec) as the
single source of truth for the routines extension, applying every
spec change that does not break the currently deployed service, and
documenting each deliberate divergence inline and in AGENTS.md.
## Spec alignment
* `RoutineRun` and `DispatchRoutineResponse` gain the new `TaskID`
field (wire `taskId`); the service already emits it. `dispatch`
now prints `Task ID` after `Action Correlation ID`, and JSON
output exposes the new field too.
* `RoutineTrigger` is restructured to match the spec's
`GitHubIssueOpenedRoutineTrigger` shape: dropped `Owner` /
`Actions[]`, added `Assignee`. The github trigger is still
deferred at the CLI surface, so this is safe.
* Inline comments and a new AGENTS.md table call out each divergence
the client deliberately keeps to stay compatible with the live
service: camelCase wire naming (spec is snake_case), `agentId`
field (spec renamed to `agent_name`), `:dispatchAsync` action
segment (spec uses `:dispatch_async`), GET+PUT enable/disable
fallback (spec adds dedicated routes which still 404),
`value`/`nextLink` / `value`/`nextPageToken` paged shapes
(spec uses `AgentsPagedResult<T>`), and `github_issue` wire
value (spec renamed to `github_issue_opened`).
## CLI bug fix: HTTP/2 stream-reset hang
The pipeline now uses a custom `http.Client` with an explicit
`ResponseHeaderTimeout` (60s) and `TryTimeout` (30s), and azcore
retries are capped at 1. When the Foundry service returns an
HTTP/2 RST_STREAM (for example, the schedule-create
InternalServerError), the CLI now surfaces a `context deadline
exceeded` error within ~40 seconds instead of the previous ~6 minute
hang.
## Verified end-to-end against a live Foundry project
* timer create / show / list / update / disable / enable / dispatch
(with `taskId` round-tripping) / run list / delete all succeed.
* schedule create still fails (service-side ISE) but now in under a
minute instead of six.
* feat(routines): defer recurring/schedule trigger until service is ready
The Foundry data plane currently returns `InternalServerError` for any
`PUT /routines/{name}` request whose trigger is `schedule` (the wire
value behind the CLI's `--trigger recurring`). The CLI side is fully
implemented and verified correct via raw curl, so this is a service-side
issue, but it leaves the `recurring` trigger non-functional end-to-end.
Take the recurring trigger off the public CLI surface so users do not
hit the service hang:
* Drop the `--cron` flag from `routine create` and `routine update`.
* `--trigger recurring` is now rejected with the same "deferred"
shape as `--trigger github-issue`: a clear error pointing the user
at `--trigger timer` and explaining that recurring is gated on the
Foundry service.
* `--trigger` help text and validation messages list only `timer`.
The underlying wire model still carries `cron_expression` /
`time_zone` and the `schedule` discriminator so re-enabling the
trigger when the service is ready is just a CLI flag-wiring change.
Unit tests around buildTrigger and applyUpdateFlags are updated
accordingly.
* fix(routines): enforce update-mode manifest merge, env-backed no-prompt in delete, and action-type flag validation
* fix(routines): replace unknown word 'misroute' in endpoint comment
* feat(routines): add exterrors unit tests and .gitignore for bin/
* style(routines): fix gofmt formatting in test files
* feat(routines): align client with spec PR #43186 routes and fields
The Foundry data plane now honors the routine spec from
azure-rest-api-specs#43186. Switch the client off the workarounds that the
first cut needed and onto the spec-shaped routes and wire format.
Tested by probing the live data plane on a Foundry project endpoint:
* Wire field naming: switch from camelCase to snake_case across Routine,
RoutineTrigger, RoutineAction, RoutineRun, and DispatchRoutineResponse.
Confirmed: service now rejects `agentId` / `agentName` (camel) with a
400 `exactly one of agent_name or agent_endpoint_id must be provided`
and only accepts `agent_name`.
* Enable / disable: switch from GET+PUT-with-enabled-flipped to the spec
routes `POST /routines/{name}:enable` and `POST /routines/{name}:disable`.
Confirmed: both routes return `UserError` / `NotFoundError` for missing
routines (route exists; resource doesn't), instead of the empty 404 the
routes used to return.
* Async dispatch: switch from `:dispatchAsync` (camelCase) to the spec route
`:dispatch_async` (snake). Confirmed: the snake route is live; the camel
form now returns an empty 404 (route gone).
* Schedule trigger: re-enable `--trigger recurring` / `--cron`. The
original deferral was because every `schedule` PUT 500'd; with the spec
wire format the schedule trigger passes service-side validation just like
`timer` does. Re-add the `--cron` flag on `create` and `update`.
Kept divergent because the service has not caught up yet:
* `github_issue_opened` trigger value -- service still rejects it with
`unrecognized type discriminator id`; CLI does not expose the github
trigger yet, so the wire mapping keeps `github_issue`.
* `AgentsPagedResult<T>` envelope -- service still returns
`value` + `nextLink` (routines) / `value` + `nextPageToken` (runs)
rather than the spec's `data` / `last_id` / `has_more`.
Also:
* CLI flag `--agent-id` renamed to `--agent-name` to match the spec
field name. Go field `RoutineAction.AgentID` renamed to `AgentName`.
* Drop now-stale `spec divergence` comments from the client, models, and
AGENTS.md alignment table.
* fix(routines): address review feedback — op code, gRPC cancel, manifest errors, logging, docs
- routine_dispatch.go: use OpDispatchRoutine (not OpGetRoutine) when the
inner GetRoutine call fails during dispatch validation
- exterrors/errors.go: IsCancellation now checks gRPC codes.Canceled in
addition to context.Canceled, matching the agents implementation
- routine_manifest.go: distinguish os.IsNotExist from other os.ReadFile
errors so permission-denied / is-a-directory get accurate messages
- client.go: add Logging.AllowedHeaders for MsCorrelationIdHeader to
match agents observability parity
- AGENTS.md: rewrite exterrors section in present tense (package exists)1 parent 6f2867c commit b429308
28 files changed
Lines changed: 3528 additions & 2 deletions
File tree
- cli/azd/extensions/azure.ai.routines
- internal
- cmd
- exterrors
- pkg/routines
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
4 | 3 | | |
5 | 4 | | |
6 | 5 | | |
| |||
15 | 14 | | |
16 | 15 | | |
17 | 16 | | |
| 17 | + | |
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
| 13 | + | |
12 | 14 | | |
13 | 15 | | |
14 | 16 | | |
| |||
0 commit comments