1import * as discoveryengine from "@distilled.cloud/gcp/discoveryengine_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import { GcpEnvironment } from "../Environment.ts";
9import type { Providers } from "../Providers.ts";
10import {
11 lastSegment,
12 parentBefore,
13 parseResourceName,
14 toResourceId,
15} from "./internal.ts";
16
17export type CollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfigProps =
18 {
19 /**
20 * A2A tenant resource name, typically the assistant or agent:
21 * `projects/{project}/locations/{location}/collections/{collection}/engines/{engine}/assistants/{assistant}`.
22 * Immutable — changing it replaces the config.
23 */
24 tenant: string;
25 /**
26 * Parent task resource (`tasks/{task}`). Immutable — changing it
27 * replaces the config.
28 */
29 parent: string;
30 /**
31 * Config id. If omitted, a unique id is generated. Immutable —
32 * changing it replaces the config.
33 */
34 configId?: string;
35 /**
36 * Notification callback URL.
37 */
38 url: string;
39 /**
40 * Token sent with the notification.
41 */
42 token?: string;
43 /**
44 * Authentication schemes (Basic, Bearer, …).
45 */
46 schemes?: string[];
47 /**
48 * Authentication credentials.
49 */
50 credentials?: string;
51 };
52
53export type CollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfig =
54 Resource<
55 "GCP.DiscoveryEngine.CollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfig",
56 CollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfigProps,
57 {
58 /** Full resource name (`tasks/{task}/pushNotificationConfigs/{config}`). */
59 name: string;
60 /** Config id (last path segment). */
61 configId: string;
62 /** A2A tenant resource name. */
63 tenant: string;
64 /** Parent task resource. */
65 parent: string;
66 /** Project id. */
67 project: string;
68 /** Notification callback URL. */
69 url: string | undefined;
70 /** Token. */
71 token: string | undefined;
72 },
73 never,
74 Providers
75 >;
76
77/**
78 * An A2A task push-notification config on a Discovery Engine Assistant
79 * agent.
80 *
81 * Experimental A2A API. There is no patch RPC, so URL / token / auth
82 * changes replace the config. The config id is deterministic: `read`
83 * reports a config it finds without prior state as unowned (adopt it
84 * with `--adopt`).
85 *
86 * ### Creating a Push Notification Config
87 * **Example:** Webhook on a task
88 * ```typescript
89 * const config =
90 * yield* GCP.DiscoveryEngine.CollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfig(
91 * "Notify",
92 * {
93 * tenant: assistant.name,
94 * parent: "tasks/support-1",
95 * url: "https://example.com/hooks/a2a",
96 * },
97 * );
98 * ```
99 *
100 * @resource
101 * @category DiscoveryEngine
102 */
103export const CollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfig =
104 Resource<CollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfig>(
105 "GCP.DiscoveryEngine.CollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfig",
106 );
107
108export class CollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfigNotResolved extends Data.TaggedError(
109 "GCP.DiscoveryEngine.CollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfigNotResolved",
110)<{
111 name: string;
112}> {}
113
114const resourceName = (parent: string, configId: string) =>
115 `${parent}/pushNotificationConfigs/${configId}`;
116
117const getByName = (tenant: string, name: string) =>
118 discoveryengine
119 .getProjectsLocationsCollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfigs(
120 { tenant, name },
121 )
122 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
123
124const toAttrs = (
125 config: discoveryengine.A2aV1TaskPushNotificationConfig,
126 project: string,
127 tenant: string,
128) => {
129 const name = config.name ?? "";
130 const parsed = parseResourceName(name, "pushNotificationConfigs");
131 return {
132 name,
133 configId: parsed.id || lastSegment(name),
134 tenant,
135 parent: parentBefore(name, "pushNotificationConfigs"),
136 project: parsed.project || project,
137 url: config.pushNotificationConfig?.url,
138 token: config.pushNotificationConfig?.token,
139 };
140};
141
142export const CollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfigProvider =
143 () =>
144 Provider.succeed(
145 CollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfig,
146 {
147 stables: ["name", "configId", "tenant", "parent", "project"],
148
149 diff: Effect.fn(function* ({ news, olds, output }) {
150 if (!isResolved(news)) return undefined;
151 const previousTenant = olds?.tenant ?? output?.tenant;
152 const previousParent = olds?.parent ?? output?.parent;
153 const previousId = olds?.configId ?? output?.configId;
154 const nextId = news.configId ?? previousId;
155 const previousUrl = olds?.url ?? output?.url;
156 if (
157 (previousTenant !== undefined && news.tenant !== previousTenant) ||
158 (previousParent !== undefined && news.parent !== previousParent) ||
159 (previousId !== undefined &&
160 nextId !== undefined &&
161 nextId !== previousId) ||
162 (previousUrl !== undefined && news.url !== previousUrl)
163 ) {
164 return { action: "replace" as const, deleteFirst: true };
165 }
166 return undefined;
167 }),
168
169 read: Effect.fn(function* ({ id, olds, output }) {
170 const env = yield* GcpEnvironment.current;
171 const tenant = olds?.tenant ?? output?.tenant;
172 const parent = olds?.parent ?? output?.parent;
173 const configId = yield* toResourceId(
174 id,
175 olds?.configId,
176 output?.configId,
177 );
178 const name =
179 output?.name ??
180 (parent !== undefined ? resourceName(parent, configId) : undefined);
181 if (tenant === undefined || name === undefined) return undefined;
182 const existing = yield* getByName(tenant, name);
183 if (existing === undefined) return undefined;
184 const attrs = toAttrs(existing, env.project, tenant);
185 // No labels field: without prior state it may not be ours.
186 return output !== undefined ? attrs : Unowned(attrs);
187 }),
188
189 reconcile: Effect.fn(function* ({ id, news, output }) {
190 const env = yield* GcpEnvironment.current;
191 const configId = yield* toResourceId(
192 id,
193 news.configId,
194 output?.configId,
195 );
196 const token = news.token;
197 const fallbackName =
198 output?.name ?? resourceName(news.parent, configId);
199
200 let current = yield* getByName(news.tenant, fallbackName);
201
202 if (current === undefined) {
203 const created = yield* discoveryengine
204 .createProjectsLocationsCollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfigs(
205 {
206 tenant: news.tenant,
207 parent: news.parent,
208 configId,
209 body: {
210 name: fallbackName,
211 pushNotificationConfig: {
212 url: news.url,
213 token,
214 id: configId,
215 authentication:
216 news.schemes !== undefined ||
217 news.credentials !== undefined
218 ? {
219 schemes: news.schemes,
220 credentials: news.credentials,
221 }
222 : undefined,
223 },
224 },
225 },
226 )
227 .pipe(
228 Effect.catchTag("Conflict", () =>
229 getByName(news.tenant, fallbackName),
230 ),
231 );
232 current = created ?? undefined;
233 }
234
235 if (current === undefined) {
236 return yield* new CollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfigNotResolved(
237 { name: fallbackName },
238 );
239 }
240
241 return toAttrs(current, env.project, news.tenant);
242 }),
243
244 delete: Effect.fn(function* ({ output }) {
245 const existing = yield* getByName(output.tenant, output.name);
246 if (existing === undefined) return;
247 yield* discoveryengine
248 .deleteProjectsLocationsCollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfigs(
249 {
250 tenant: output.tenant,
251 name: output.name,
252 },
253 )
254 .pipe(Effect.catchTag("NotFound", () => Effect.void));
255 }),
256 },
257 );