@consiliency/agent-board-schema 1.2.0
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.
- package/LICENSE +202 -0
- package/NOTICE +11 -0
- package/README.md +55 -0
- package/bin/agent-board-schema.js +97 -0
- package/fixtures/already-applied.json +181 -0
- package/fixtures/divergent-history.json +173 -0
- package/fixtures/production-history.json +173 -0
- package/migrations/20260501000001_agent_board_schema_version.sql +12 -0
- package/migrations/20260502000000_agent_board_schema.sql +180 -0
- package/migrations/20260503000000_agent_board_rpcs.sql +422 -0
- package/migrations/20260503000001_agent_board_coordination_rpcs.sql +229 -0
- package/migrations/20260504000000_agent_board_read_models.sql +206 -0
- package/migrations/20260505000000_agent_board_inbox_schema.sql +341 -0
- package/migrations/20260506000000_agent_board_inbox_delivery_rpcs.sql +1200 -0
- package/migrations/20260506000001_agent_board_inbox_callback_rpcs.sql +330 -0
- package/migrations/20260507000000_agent_board_inbox_reads.sql +334 -0
- package/migrations/20260508000000_agent_board_callback_runtime.sql +453 -0
- package/migrations/20260508000001_agent_board_inbox_service_role_reads.sql +10 -0
- package/migrations/20260510000000_agent_board_dblint_repairs.sql +413 -0
- package/migrations/20260511000000_agent_board_auth_schema.sql +146 -0
- package/migrations/20260511000001_agent_board_auth_credentials.sql +39 -0
- package/migrations/20260512000000_agent_board_auth_provisioning_rpcs.sql +622 -0
- package/migrations/20260512000001_agent_board_auth_v1_rpcs.sql +798 -0
- package/migrations/20260512000002_agent_board_auth_v2_rpcs.sql +1243 -0
- package/migrations/20260512000003_agent_board_auth_rls.sql +484 -0
- package/migrations/20260513000000_agent_board_backup_ops.sql +34 -0
- package/migrations/20260513000001_agent_board_retention.sql +160 -0
- package/migrations/20260513000002_agent_board_monitoring_views.sql +227 -0
- package/migrations/20260514000000_agent_board_auth_rls_id_qualification.sql +150 -0
- package/migrations/20260515000000_agent_board_boardopt.sql +493 -0
- package/migrations/20260516000000_agent_board_callbackcap.sql +113 -0
- package/migrations/20260517000000_agent_board_addressing.sql +542 -0
- package/migrations/20260628000000_agent_board_signing_store.sql +174 -0
- package/migrations/20260628000001_agent_board_signing_proof_rpcs.sql +215 -0
- package/migrations/20260628000002_agent_board_signature_rls_reads.sql +300 -0
- package/migrations/20260628000003_agent_board_signed_write_gateway.sql +288 -0
- package/migrations/20260628000004_agent_board_fleet_registry.sql +411 -0
- package/migrations/20260628000005_agent_board_fleet_policy.sql +509 -0
- package/migrations/20260628000006_agent_board_fleet_read_models.sql +170 -0
- package/migrations/20260629000100_agent_board_fleet_registry_repair.sql +411 -0
- package/migrations/20260629000101_agent_board_fleet_policy_refresh.sql +509 -0
- package/migrations/20260629000102_agent_board_fleet_read_models_refresh.sql +170 -0
- package/migrations/20260630000100_agent_board_harness_kind_open_vocab.sql +15 -0
- package/migrations/20260710000000_agent_board_authbound_boundary.sql +155 -0
- package/migrations/20260710000100_agent_board_schema_version_rpc.sql +14 -0
- package/migrations/20260710000400_agent_board_nonfleet_identity.sql +87 -0
- package/migrations/20260710000401_agent_board_workload_providers.sql +39 -0
- package/migrations/20260711000000_agent_board_provision_system_fail_closed.sql +242 -0
- package/migrations/20260711000100_canary_signed_fleet_enforcement.sql +234 -0
- package/migrations/20260712000000_agent_board_bundle_endpoint_compat.sql +97 -0
- package/migrations/20260713000000_agent_board_rls_reassert_board_scoping.sql +90 -0
- package/package.json +42 -0
- package/provenance.json +580 -0
- package/schema-manifest.json +458 -0
- package/src/check.js +23 -0
- package/src/index.js +50 -0
- package/src/materialize.js +73 -0
- package/src/plan.js +44 -0
- package/src/tooling.test.mjs +97 -0
- package/tests/agent_board_addressing.test.sql +166 -0
- package/tests/agent_board_auth_rls.test.sql +243 -0
- package/tests/agent_board_auth_rls_id_qualification.test.sql +225 -0
- package/tests/agent_board_auth_rpcs.test.sql +311 -0
- package/tests/agent_board_auth_schema.test.sql +411 -0
- package/tests/agent_board_authbound_boundary.test.sql +148 -0
- package/tests/agent_board_boardopt.test.sql +170 -0
- package/tests/agent_board_bundle_endpoint_compat.test.sql +49 -0
- package/tests/agent_board_callback_runtime.test.sql +425 -0
- package/tests/agent_board_callbackcap.test.sql +222 -0
- package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +60 -0
- package/tests/agent_board_dblint_rpcs.test.sql +247 -0
- package/tests/agent_board_fleet_policy.test.sql +370 -0
- package/tests/agent_board_fleet_read_models.test.sql +158 -0
- package/tests/agent_board_fleet_registry.test.sql +149 -0
- package/tests/agent_board_inbox_reads.test.sql +471 -0
- package/tests/agent_board_inbox_rpcs.test.sql +452 -0
- package/tests/agent_board_inbox_schema.test.sql +355 -0
- package/tests/agent_board_nonfleet_identity.test.sql +26 -0
- package/tests/agent_board_rls_reassert_board_scoping.test.sql +39 -0
- package/tests/agent_board_schema_version.test.sql +24 -0
- package/tests/agent_board_schema_version_rpc.test.sql +40 -0
- package/tests/agent_board_signature_rls_reads.test.sql +133 -0
- package/tests/agent_board_signed_write_gateway.test.sql +219 -0
- package/tests/agent_board_signing_proof_rpcs.test.sql +143 -0
- package/tests/agent_board_signing_store.test.sql +183 -0
- package/tests/agent_board_smoke.test.sql +12 -0
- package/tests/agent_board_v1_invariants.test.sql +298 -0
- package/tests/agent_board_v2_invariants.test.sql +753 -0
- package/tests/agent_board_workload_providers.test.sql +15 -0
- package/tests/fixtures/adopter-base.sql +19 -0
- package/tests/fixtures/operator-hardening.sql +27 -0
- package/tests/fixtures/test-prereqs.sql +84 -0
|
@@ -0,0 +1,1200 @@
|
|
|
1
|
+
create or replace function agent_board._trim_inbox_text(p_value text, p_max_length integer)
|
|
2
|
+
returns text
|
|
3
|
+
language sql
|
|
4
|
+
immutable
|
|
5
|
+
as $$
|
|
6
|
+
select case
|
|
7
|
+
when p_value is null then null
|
|
8
|
+
else nullif(left(trim(p_value), p_max_length), '')
|
|
9
|
+
end
|
|
10
|
+
$$;
|
|
11
|
+
|
|
12
|
+
create or replace function agent_board._lock_idempotency_key(p_namespace text, p_idempotency_key text)
|
|
13
|
+
returns void
|
|
14
|
+
language plpgsql
|
|
15
|
+
security definer
|
|
16
|
+
set search_path = agent_board, public
|
|
17
|
+
as $$
|
|
18
|
+
begin
|
|
19
|
+
if nullif(trim(p_idempotency_key), '') is null then
|
|
20
|
+
return;
|
|
21
|
+
end if;
|
|
22
|
+
|
|
23
|
+
perform pg_advisory_xact_lock(hashtextextended(p_namespace || ':' || trim(p_idempotency_key), 0));
|
|
24
|
+
end;
|
|
25
|
+
$$;
|
|
26
|
+
|
|
27
|
+
create or replace function agent_board._build_fetch_descriptor(
|
|
28
|
+
p_surface text,
|
|
29
|
+
p_target_id uuid,
|
|
30
|
+
p_view text,
|
|
31
|
+
p_reason text,
|
|
32
|
+
p_filters jsonb default '{}'::jsonb
|
|
33
|
+
)
|
|
34
|
+
returns jsonb
|
|
35
|
+
language sql
|
|
36
|
+
immutable
|
|
37
|
+
as $$
|
|
38
|
+
select jsonb_build_object(
|
|
39
|
+
'surface', p_surface,
|
|
40
|
+
'targetId', p_target_id::text,
|
|
41
|
+
'view', p_view,
|
|
42
|
+
'filters', coalesce(p_filters, '{}'::jsonb),
|
|
43
|
+
'reason', p_reason
|
|
44
|
+
)
|
|
45
|
+
$$;
|
|
46
|
+
|
|
47
|
+
create or replace function agent_board._active_board_membership_id(p_board_id uuid, p_agent_id uuid)
|
|
48
|
+
returns uuid
|
|
49
|
+
language sql
|
|
50
|
+
stable
|
|
51
|
+
as $$
|
|
52
|
+
select bm.id
|
|
53
|
+
from agent_board.board_memberships bm
|
|
54
|
+
where bm.board_id = p_board_id
|
|
55
|
+
and bm.agent_id = p_agent_id
|
|
56
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
57
|
+
order by bm.created_at desc
|
|
58
|
+
limit 1
|
|
59
|
+
$$;
|
|
60
|
+
|
|
61
|
+
create or replace function agent_board._upsert_delivery_recipient(
|
|
62
|
+
p_delivery_mode text,
|
|
63
|
+
p_recipient_agent_id uuid,
|
|
64
|
+
p_source_kind text,
|
|
65
|
+
p_source_message_id uuid default null,
|
|
66
|
+
p_source_task_id uuid default null,
|
|
67
|
+
p_source_delegation_id uuid default null,
|
|
68
|
+
p_board_id uuid default null,
|
|
69
|
+
p_board_membership_id uuid default null,
|
|
70
|
+
p_metadata jsonb default '{}'::jsonb
|
|
71
|
+
)
|
|
72
|
+
returns agent_board.delivery_recipients
|
|
73
|
+
language plpgsql
|
|
74
|
+
security definer
|
|
75
|
+
set search_path = agent_board, public
|
|
76
|
+
as $$
|
|
77
|
+
declare
|
|
78
|
+
v_row agent_board.delivery_recipients%rowtype;
|
|
79
|
+
begin
|
|
80
|
+
insert into agent_board.delivery_recipients(
|
|
81
|
+
delivery_mode,
|
|
82
|
+
recipient_agent_id,
|
|
83
|
+
source_kind,
|
|
84
|
+
source_message_id,
|
|
85
|
+
source_task_id,
|
|
86
|
+
source_delegation_id,
|
|
87
|
+
board_id,
|
|
88
|
+
board_membership_id,
|
|
89
|
+
metadata
|
|
90
|
+
)
|
|
91
|
+
values (
|
|
92
|
+
p_delivery_mode,
|
|
93
|
+
p_recipient_agent_id,
|
|
94
|
+
p_source_kind,
|
|
95
|
+
p_source_message_id,
|
|
96
|
+
p_source_task_id,
|
|
97
|
+
p_source_delegation_id,
|
|
98
|
+
p_board_id,
|
|
99
|
+
p_board_membership_id,
|
|
100
|
+
coalesce(p_metadata, '{}'::jsonb)
|
|
101
|
+
)
|
|
102
|
+
on conflict (recipient_agent_id, canonical_source_key) do update
|
|
103
|
+
set metadata = coalesce(agent_board.delivery_recipients.metadata, '{}'::jsonb) || coalesce(excluded.metadata, '{}'::jsonb)
|
|
104
|
+
returning * into v_row;
|
|
105
|
+
|
|
106
|
+
return v_row;
|
|
107
|
+
end;
|
|
108
|
+
$$;
|
|
109
|
+
|
|
110
|
+
create or replace function agent_board._upsert_inbox_item(
|
|
111
|
+
p_delivery_recipient_id uuid,
|
|
112
|
+
p_recipient_agent_id uuid,
|
|
113
|
+
p_board_id uuid,
|
|
114
|
+
p_delivery_mode text,
|
|
115
|
+
p_source_kind text,
|
|
116
|
+
p_source_message_id uuid default null,
|
|
117
|
+
p_source_task_id uuid default null,
|
|
118
|
+
p_source_delegation_id uuid default null,
|
|
119
|
+
p_subject text default null,
|
|
120
|
+
p_preview text default null,
|
|
121
|
+
p_priority text default 'medium',
|
|
122
|
+
p_visible_in_inbox boolean default true,
|
|
123
|
+
p_visible_in_board_feed boolean default false,
|
|
124
|
+
p_fetch_descriptor jsonb default '{}'::jsonb,
|
|
125
|
+
p_metadata jsonb default '{}'::jsonb
|
|
126
|
+
)
|
|
127
|
+
returns agent_board.inbox_items
|
|
128
|
+
language plpgsql
|
|
129
|
+
security definer
|
|
130
|
+
set search_path = agent_board, public
|
|
131
|
+
as $$
|
|
132
|
+
declare
|
|
133
|
+
v_row agent_board.inbox_items%rowtype;
|
|
134
|
+
begin
|
|
135
|
+
insert into agent_board.inbox_items(
|
|
136
|
+
delivery_recipient_id,
|
|
137
|
+
recipient_agent_id,
|
|
138
|
+
board_id,
|
|
139
|
+
delivery_mode,
|
|
140
|
+
source_kind,
|
|
141
|
+
source_message_id,
|
|
142
|
+
source_task_id,
|
|
143
|
+
source_delegation_id,
|
|
144
|
+
subject,
|
|
145
|
+
preview,
|
|
146
|
+
priority,
|
|
147
|
+
visible_in_inbox,
|
|
148
|
+
visible_in_board_feed,
|
|
149
|
+
fetch_descriptor,
|
|
150
|
+
metadata
|
|
151
|
+
)
|
|
152
|
+
values (
|
|
153
|
+
p_delivery_recipient_id,
|
|
154
|
+
p_recipient_agent_id,
|
|
155
|
+
p_board_id,
|
|
156
|
+
p_delivery_mode,
|
|
157
|
+
p_source_kind,
|
|
158
|
+
p_source_message_id,
|
|
159
|
+
p_source_task_id,
|
|
160
|
+
p_source_delegation_id,
|
|
161
|
+
p_subject,
|
|
162
|
+
p_preview,
|
|
163
|
+
p_priority,
|
|
164
|
+
p_visible_in_inbox,
|
|
165
|
+
p_visible_in_board_feed,
|
|
166
|
+
coalesce(p_fetch_descriptor, '{}'::jsonb),
|
|
167
|
+
coalesce(p_metadata, '{}'::jsonb)
|
|
168
|
+
)
|
|
169
|
+
on conflict (delivery_recipient_id) do update
|
|
170
|
+
set board_id = excluded.board_id,
|
|
171
|
+
subject = excluded.subject,
|
|
172
|
+
preview = excluded.preview,
|
|
173
|
+
priority = excluded.priority,
|
|
174
|
+
visible_in_inbox = excluded.visible_in_inbox,
|
|
175
|
+
visible_in_board_feed = excluded.visible_in_board_feed,
|
|
176
|
+
fetch_descriptor = excluded.fetch_descriptor,
|
|
177
|
+
metadata = coalesce(agent_board.inbox_items.metadata, '{}'::jsonb) || coalesce(excluded.metadata, '{}'::jsonb),
|
|
178
|
+
updated_at = now()
|
|
179
|
+
returning * into v_row;
|
|
180
|
+
|
|
181
|
+
return v_row;
|
|
182
|
+
end;
|
|
183
|
+
$$;
|
|
184
|
+
|
|
185
|
+
create or replace function agent_board._queue_inbox_callbacks(p_inbox_item_id uuid, p_event_category text)
|
|
186
|
+
returns integer
|
|
187
|
+
language plpgsql
|
|
188
|
+
security definer
|
|
189
|
+
set search_path = agent_board, public
|
|
190
|
+
as $$
|
|
191
|
+
declare
|
|
192
|
+
v_inserted_count integer := 0;
|
|
193
|
+
begin
|
|
194
|
+
if p_event_category not in ('inbox.item.delivered', 'inbox.item.updated') then
|
|
195
|
+
raise exception 'unsupported inbox callback event category' using errcode = '22023';
|
|
196
|
+
end if;
|
|
197
|
+
|
|
198
|
+
with inserted as (
|
|
199
|
+
insert into agent_board.callback_outbox_events(
|
|
200
|
+
endpoint_id,
|
|
201
|
+
subscription_id,
|
|
202
|
+
recipient_agent_id,
|
|
203
|
+
inbox_item_id,
|
|
204
|
+
board_id,
|
|
205
|
+
event_category,
|
|
206
|
+
delivery_mode,
|
|
207
|
+
source_kind,
|
|
208
|
+
source_message_id,
|
|
209
|
+
source_task_id,
|
|
210
|
+
source_delegation_id,
|
|
211
|
+
idempotency_key,
|
|
212
|
+
subject,
|
|
213
|
+
preview,
|
|
214
|
+
priority,
|
|
215
|
+
fetch_descriptor,
|
|
216
|
+
metadata
|
|
217
|
+
)
|
|
218
|
+
select
|
|
219
|
+
cs.endpoint_id,
|
|
220
|
+
cs.id,
|
|
221
|
+
ii.recipient_agent_id,
|
|
222
|
+
ii.id,
|
|
223
|
+
null,
|
|
224
|
+
p_event_category,
|
|
225
|
+
ii.delivery_mode,
|
|
226
|
+
ii.source_kind,
|
|
227
|
+
ii.source_message_id,
|
|
228
|
+
ii.source_task_id,
|
|
229
|
+
ii.source_delegation_id,
|
|
230
|
+
case
|
|
231
|
+
when p_event_category = 'inbox.item.delivered' then
|
|
232
|
+
p_event_category || ':' || ii.id::text || ':' || cs.id::text
|
|
233
|
+
else
|
|
234
|
+
p_event_category || ':' || ii.id::text || ':' || cs.id::text || ':' || replace(ii.updated_at::text, ' ', 'T') || ':' || ii.action_state
|
|
235
|
+
end,
|
|
236
|
+
ii.subject,
|
|
237
|
+
ii.preview,
|
|
238
|
+
ii.priority,
|
|
239
|
+
ii.fetch_descriptor,
|
|
240
|
+
jsonb_build_object(
|
|
241
|
+
'subscriptionScope', cs.subscription_scope,
|
|
242
|
+
'subscriptionStatus', cs.subscription_status,
|
|
243
|
+
'actionState', ii.action_state
|
|
244
|
+
)
|
|
245
|
+
from agent_board.inbox_items ii
|
|
246
|
+
join agent_board.callback_subscriptions cs
|
|
247
|
+
on cs.agent_id = ii.recipient_agent_id
|
|
248
|
+
and cs.subscription_scope = 'own_inbox'
|
|
249
|
+
and cs.subscription_status = 'active'
|
|
250
|
+
and cs.event_category = p_event_category
|
|
251
|
+
join agent_board.callback_endpoints ce
|
|
252
|
+
on ce.id = cs.endpoint_id
|
|
253
|
+
and ce.owner_agent_id = cs.agent_id
|
|
254
|
+
and ce.endpoint_status = 'verified'
|
|
255
|
+
where ii.id = p_inbox_item_id
|
|
256
|
+
on conflict (idempotency_key) do nothing
|
|
257
|
+
returning 1
|
|
258
|
+
)
|
|
259
|
+
select count(*) into v_inserted_count from inserted;
|
|
260
|
+
|
|
261
|
+
return v_inserted_count;
|
|
262
|
+
end;
|
|
263
|
+
$$;
|
|
264
|
+
|
|
265
|
+
create or replace function agent_board._queue_board_post_callbacks(
|
|
266
|
+
p_board_id uuid,
|
|
267
|
+
p_message_id uuid,
|
|
268
|
+
p_subject text,
|
|
269
|
+
p_preview text,
|
|
270
|
+
p_priority text,
|
|
271
|
+
p_fetch_descriptor jsonb
|
|
272
|
+
)
|
|
273
|
+
returns integer
|
|
274
|
+
language plpgsql
|
|
275
|
+
security definer
|
|
276
|
+
set search_path = agent_board, public
|
|
277
|
+
as $$
|
|
278
|
+
declare
|
|
279
|
+
v_inserted_count integer := 0;
|
|
280
|
+
begin
|
|
281
|
+
with inserted as (
|
|
282
|
+
insert into agent_board.callback_outbox_events(
|
|
283
|
+
endpoint_id,
|
|
284
|
+
subscription_id,
|
|
285
|
+
recipient_agent_id,
|
|
286
|
+
inbox_item_id,
|
|
287
|
+
board_id,
|
|
288
|
+
event_category,
|
|
289
|
+
delivery_mode,
|
|
290
|
+
source_kind,
|
|
291
|
+
source_message_id,
|
|
292
|
+
idempotency_key,
|
|
293
|
+
subject,
|
|
294
|
+
preview,
|
|
295
|
+
priority,
|
|
296
|
+
fetch_descriptor,
|
|
297
|
+
metadata
|
|
298
|
+
)
|
|
299
|
+
select
|
|
300
|
+
cs.endpoint_id,
|
|
301
|
+
cs.id,
|
|
302
|
+
cs.agent_id,
|
|
303
|
+
null,
|
|
304
|
+
p_board_id,
|
|
305
|
+
'board.post.delivered',
|
|
306
|
+
'board',
|
|
307
|
+
'message',
|
|
308
|
+
p_message_id,
|
|
309
|
+
'board.post.delivered:' || p_message_id::text || ':' || cs.id::text,
|
|
310
|
+
p_subject,
|
|
311
|
+
p_preview,
|
|
312
|
+
p_priority,
|
|
313
|
+
p_fetch_descriptor,
|
|
314
|
+
jsonb_build_object(
|
|
315
|
+
'subscriptionScope', cs.subscription_scope,
|
|
316
|
+
'subscriptionStatus', cs.subscription_status
|
|
317
|
+
)
|
|
318
|
+
from agent_board.callback_subscriptions cs
|
|
319
|
+
join agent_board.callback_endpoints ce
|
|
320
|
+
on ce.id = cs.endpoint_id
|
|
321
|
+
and ce.owner_agent_id = cs.agent_id
|
|
322
|
+
and ce.endpoint_status = 'verified'
|
|
323
|
+
join agent_board.board_memberships bm
|
|
324
|
+
on bm.id = cs.board_membership_id
|
|
325
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
326
|
+
where cs.subscription_scope = 'board'
|
|
327
|
+
and cs.subscription_status = 'active'
|
|
328
|
+
and cs.event_category = 'board.post.delivered'
|
|
329
|
+
and cs.board_id = p_board_id
|
|
330
|
+
on conflict (idempotency_key) do nothing
|
|
331
|
+
returning 1
|
|
332
|
+
)
|
|
333
|
+
select count(*) into v_inserted_count from inserted;
|
|
334
|
+
|
|
335
|
+
return v_inserted_count;
|
|
336
|
+
end;
|
|
337
|
+
$$;
|
|
338
|
+
|
|
339
|
+
create or replace function agent_board.create_board(
|
|
340
|
+
p_created_by_agent_id uuid,
|
|
341
|
+
p_slug text,
|
|
342
|
+
p_name text,
|
|
343
|
+
p_description text default null,
|
|
344
|
+
p_metadata jsonb default '{}'::jsonb
|
|
345
|
+
)
|
|
346
|
+
returns table(board_id uuid, slug text, name text, status text, created_at timestamptz, updated_at timestamptz)
|
|
347
|
+
language plpgsql
|
|
348
|
+
security definer
|
|
349
|
+
set search_path = agent_board, public
|
|
350
|
+
as $$
|
|
351
|
+
begin
|
|
352
|
+
if nullif(trim(p_slug), '') is null or nullif(trim(p_name), '') is null then
|
|
353
|
+
raise exception 'board slug and name are required' using errcode = '22023';
|
|
354
|
+
end if;
|
|
355
|
+
if not exists (select 1 from agent_board.agents where id = p_created_by_agent_id) then
|
|
356
|
+
raise exception 'agent not found' using errcode = 'P0002';
|
|
357
|
+
end if;
|
|
358
|
+
|
|
359
|
+
return query
|
|
360
|
+
insert into agent_board.boards as b(slug, name, description, created_by_agent_id, metadata)
|
|
361
|
+
values (trim(p_slug), trim(p_name), nullif(trim(p_description), ''), p_created_by_agent_id, coalesce(p_metadata, '{}'::jsonb))
|
|
362
|
+
on conflict on constraint boards_slug_key do update
|
|
363
|
+
set name = excluded.name,
|
|
364
|
+
description = excluded.description,
|
|
365
|
+
status = 'active',
|
|
366
|
+
metadata = coalesce(b.metadata, '{}'::jsonb) || coalesce(excluded.metadata, '{}'::jsonb),
|
|
367
|
+
updated_at = now()
|
|
368
|
+
returning b.id, b.slug, b.name, b.status, b.created_at, b.updated_at;
|
|
369
|
+
end;
|
|
370
|
+
$$;
|
|
371
|
+
|
|
372
|
+
create or replace function agent_board.join_board(
|
|
373
|
+
p_board_id uuid,
|
|
374
|
+
p_agent_id uuid,
|
|
375
|
+
p_membership_state text default 'joined',
|
|
376
|
+
p_created_by_agent_id uuid default null,
|
|
377
|
+
p_metadata jsonb default '{}'::jsonb
|
|
378
|
+
)
|
|
379
|
+
returns table(membership_id uuid, board_id uuid, agent_id uuid, membership_state text, created_at timestamptz, updated_at timestamptz)
|
|
380
|
+
language plpgsql
|
|
381
|
+
security definer
|
|
382
|
+
set search_path = agent_board, public
|
|
383
|
+
as $$
|
|
384
|
+
declare
|
|
385
|
+
v_membership agent_board.board_memberships%rowtype;
|
|
386
|
+
begin
|
|
387
|
+
if p_membership_state not in ('joined', 'subscribed') then
|
|
388
|
+
raise exception 'join_board only accepts joined or subscribed' using errcode = '22023';
|
|
389
|
+
end if;
|
|
390
|
+
if not exists (select 1 from agent_board.boards where id = p_board_id) then
|
|
391
|
+
raise exception 'board not found' using errcode = 'P0002';
|
|
392
|
+
end if;
|
|
393
|
+
if not exists (select 1 from agent_board.agents where id = p_agent_id) then
|
|
394
|
+
raise exception 'agent not found' using errcode = 'P0002';
|
|
395
|
+
end if;
|
|
396
|
+
if p_created_by_agent_id is not null and not exists (select 1 from agent_board.agents where id = p_created_by_agent_id) then
|
|
397
|
+
raise exception 'creator agent not found' using errcode = 'P0002';
|
|
398
|
+
end if;
|
|
399
|
+
|
|
400
|
+
select *
|
|
401
|
+
into v_membership
|
|
402
|
+
from agent_board.board_memberships bm
|
|
403
|
+
where bm.board_id = p_board_id
|
|
404
|
+
and bm.agent_id = p_agent_id
|
|
405
|
+
order by bm.created_at desc
|
|
406
|
+
limit 1
|
|
407
|
+
for update;
|
|
408
|
+
|
|
409
|
+
if v_membership.id is null then
|
|
410
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, metadata)
|
|
411
|
+
values (p_board_id, p_agent_id, p_membership_state, coalesce(p_created_by_agent_id, p_agent_id), coalesce(p_metadata, '{}'::jsonb))
|
|
412
|
+
returning * into v_membership;
|
|
413
|
+
else
|
|
414
|
+
update agent_board.board_memberships bm
|
|
415
|
+
set membership_state = p_membership_state,
|
|
416
|
+
left_at = null,
|
|
417
|
+
metadata = coalesce(bm.metadata, '{}'::jsonb) || coalesce(p_metadata, '{}'::jsonb),
|
|
418
|
+
updated_at = now()
|
|
419
|
+
where bm.id = v_membership.id
|
|
420
|
+
returning * into v_membership;
|
|
421
|
+
end if;
|
|
422
|
+
|
|
423
|
+
return query
|
|
424
|
+
select v_membership.id, v_membership.board_id, v_membership.agent_id, v_membership.membership_state, v_membership.created_at, v_membership.updated_at;
|
|
425
|
+
end;
|
|
426
|
+
$$;
|
|
427
|
+
|
|
428
|
+
create or replace function agent_board.update_board_membership(
|
|
429
|
+
p_board_id uuid,
|
|
430
|
+
p_agent_id uuid,
|
|
431
|
+
p_membership_state text,
|
|
432
|
+
p_metadata jsonb default '{}'::jsonb
|
|
433
|
+
)
|
|
434
|
+
returns table(membership_id uuid, board_id uuid, agent_id uuid, membership_state text, left_at timestamptz, updated_at timestamptz)
|
|
435
|
+
language plpgsql
|
|
436
|
+
security definer
|
|
437
|
+
set search_path = agent_board, public
|
|
438
|
+
as $$
|
|
439
|
+
declare
|
|
440
|
+
v_membership agent_board.board_memberships%rowtype;
|
|
441
|
+
begin
|
|
442
|
+
if p_membership_state not in ('joined', 'subscribed', 'left', 'removed') then
|
|
443
|
+
raise exception 'invalid board membership state' using errcode = '22023';
|
|
444
|
+
end if;
|
|
445
|
+
|
|
446
|
+
select *
|
|
447
|
+
into v_membership
|
|
448
|
+
from agent_board.board_memberships bm
|
|
449
|
+
where bm.board_id = p_board_id
|
|
450
|
+
and bm.agent_id = p_agent_id
|
|
451
|
+
order by bm.created_at desc
|
|
452
|
+
limit 1
|
|
453
|
+
for update;
|
|
454
|
+
|
|
455
|
+
if v_membership.id is null then
|
|
456
|
+
raise exception 'board membership not found' using errcode = 'P0002';
|
|
457
|
+
end if;
|
|
458
|
+
|
|
459
|
+
update agent_board.board_memberships bm
|
|
460
|
+
set membership_state = p_membership_state,
|
|
461
|
+
left_at = case when p_membership_state in ('left', 'removed') then now() else null end,
|
|
462
|
+
metadata = coalesce(bm.metadata, '{}'::jsonb) || coalesce(p_metadata, '{}'::jsonb),
|
|
463
|
+
updated_at = now()
|
|
464
|
+
where bm.id = v_membership.id
|
|
465
|
+
returning * into v_membership;
|
|
466
|
+
|
|
467
|
+
return query
|
|
468
|
+
select v_membership.id, v_membership.board_id, v_membership.agent_id, v_membership.membership_state, v_membership.left_at, v_membership.updated_at;
|
|
469
|
+
end;
|
|
470
|
+
$$;
|
|
471
|
+
|
|
472
|
+
create or replace function agent_board.leave_board(
|
|
473
|
+
p_board_id uuid,
|
|
474
|
+
p_agent_id uuid,
|
|
475
|
+
p_metadata jsonb default '{}'::jsonb
|
|
476
|
+
)
|
|
477
|
+
returns table(membership_id uuid, board_id uuid, agent_id uuid, membership_state text, left_at timestamptz, updated_at timestamptz)
|
|
478
|
+
language plpgsql
|
|
479
|
+
security definer
|
|
480
|
+
set search_path = agent_board, public
|
|
481
|
+
as $$
|
|
482
|
+
begin
|
|
483
|
+
return query
|
|
484
|
+
select *
|
|
485
|
+
from agent_board.update_board_membership(p_board_id, p_agent_id, 'left', p_metadata);
|
|
486
|
+
end;
|
|
487
|
+
$$;
|
|
488
|
+
|
|
489
|
+
create or replace function agent_board.send_direct_message(
|
|
490
|
+
p_thread_id uuid,
|
|
491
|
+
p_author_agent_id uuid,
|
|
492
|
+
p_recipient_agent_id uuid,
|
|
493
|
+
p_subject text,
|
|
494
|
+
p_body text,
|
|
495
|
+
p_message_type text default 'comment',
|
|
496
|
+
p_priority text default 'medium',
|
|
497
|
+
p_idempotency_key text default null,
|
|
498
|
+
p_metadata jsonb default '{}'::jsonb
|
|
499
|
+
)
|
|
500
|
+
returns table(message_id uuid, delivery_recipient_id uuid, inbox_item_id uuid, callback_outbox_count integer, created_at timestamptz)
|
|
501
|
+
language plpgsql
|
|
502
|
+
security definer
|
|
503
|
+
set search_path = agent_board, public
|
|
504
|
+
as $$
|
|
505
|
+
declare
|
|
506
|
+
v_message agent_board.messages%rowtype;
|
|
507
|
+
v_created_message record;
|
|
508
|
+
v_recipient agent_board.delivery_recipients%rowtype;
|
|
509
|
+
v_inbox agent_board.inbox_items%rowtype;
|
|
510
|
+
v_callback_count integer := 0;
|
|
511
|
+
v_metadata jsonb;
|
|
512
|
+
v_preview text;
|
|
513
|
+
v_fetch_descriptor jsonb;
|
|
514
|
+
v_idempotency_key text := nullif(trim(p_idempotency_key), '');
|
|
515
|
+
begin
|
|
516
|
+
if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
|
|
517
|
+
raise exception 'subject and body are required' using errcode = '22023';
|
|
518
|
+
end if;
|
|
519
|
+
if p_priority not in ('low', 'medium', 'high', 'urgent') then
|
|
520
|
+
raise exception 'invalid priority' using errcode = '22023';
|
|
521
|
+
end if;
|
|
522
|
+
if not exists (select 1 from agent_board.threads where id = p_thread_id) then
|
|
523
|
+
raise exception 'thread not found' using errcode = 'P0002';
|
|
524
|
+
end if;
|
|
525
|
+
if not exists (select 1 from agent_board.agents where id = p_author_agent_id) then
|
|
526
|
+
raise exception 'author agent not found' using errcode = 'P0002';
|
|
527
|
+
end if;
|
|
528
|
+
if not exists (select 1 from agent_board.agents where id = p_recipient_agent_id) then
|
|
529
|
+
raise exception 'recipient agent not found' using errcode = 'P0002';
|
|
530
|
+
end if;
|
|
531
|
+
|
|
532
|
+
perform agent_board._lock_idempotency_key(
|
|
533
|
+
'send_direct_message:' || p_thread_id::text || ':' || p_author_agent_id::text,
|
|
534
|
+
v_idempotency_key
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
if v_idempotency_key is not null then
|
|
538
|
+
select *
|
|
539
|
+
into v_message
|
|
540
|
+
from agent_board.messages
|
|
541
|
+
where thread_id = p_thread_id
|
|
542
|
+
and author_agent_id = p_author_agent_id
|
|
543
|
+
and metadata ->> 'rpcName' = 'send_direct_message'
|
|
544
|
+
and metadata ->> 'idempotencyKey' = v_idempotency_key
|
|
545
|
+
order by created_at desc
|
|
546
|
+
limit 1;
|
|
547
|
+
end if;
|
|
548
|
+
|
|
549
|
+
if v_message.id is null then
|
|
550
|
+
v_metadata := coalesce(p_metadata, '{}'::jsonb)
|
|
551
|
+
|| jsonb_build_object(
|
|
552
|
+
'rpcName', 'send_direct_message',
|
|
553
|
+
'subject', trim(p_subject),
|
|
554
|
+
'priority', p_priority
|
|
555
|
+
);
|
|
556
|
+
if v_idempotency_key is not null then
|
|
557
|
+
v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
|
|
558
|
+
end if;
|
|
559
|
+
|
|
560
|
+
select *
|
|
561
|
+
into v_created_message
|
|
562
|
+
from agent_board.post_message(
|
|
563
|
+
p_thread_id,
|
|
564
|
+
p_author_agent_id,
|
|
565
|
+
p_body,
|
|
566
|
+
p_message_type,
|
|
567
|
+
null,
|
|
568
|
+
v_metadata
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
select *
|
|
572
|
+
into v_message
|
|
573
|
+
from agent_board.messages
|
|
574
|
+
where id = v_created_message.message_id;
|
|
575
|
+
end if;
|
|
576
|
+
|
|
577
|
+
v_preview := agent_board._trim_inbox_text(p_body, 280);
|
|
578
|
+
v_fetch_descriptor := agent_board._build_fetch_descriptor(
|
|
579
|
+
'thread',
|
|
580
|
+
p_thread_id,
|
|
581
|
+
'agent_board.v_thread_feed',
|
|
582
|
+
'direct delivery',
|
|
583
|
+
jsonb_build_object('messageId', v_message.id::text, 'recipientAgentId', p_recipient_agent_id::text)
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
v_recipient := agent_board._upsert_delivery_recipient(
|
|
587
|
+
'direct',
|
|
588
|
+
p_recipient_agent_id,
|
|
589
|
+
'message',
|
|
590
|
+
v_message.id,
|
|
591
|
+
null,
|
|
592
|
+
null,
|
|
593
|
+
null,
|
|
594
|
+
null,
|
|
595
|
+
jsonb_build_object('priority', p_priority)
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
v_inbox := agent_board._upsert_inbox_item(
|
|
599
|
+
v_recipient.id,
|
|
600
|
+
p_recipient_agent_id,
|
|
601
|
+
null,
|
|
602
|
+
v_recipient.delivery_mode,
|
|
603
|
+
'message',
|
|
604
|
+
v_message.id,
|
|
605
|
+
null,
|
|
606
|
+
null,
|
|
607
|
+
trim(p_subject),
|
|
608
|
+
v_preview,
|
|
609
|
+
p_priority,
|
|
610
|
+
true,
|
|
611
|
+
false,
|
|
612
|
+
v_fetch_descriptor,
|
|
613
|
+
jsonb_build_object('threadId', p_thread_id::text)
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
v_callback_count := agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
|
|
617
|
+
|
|
618
|
+
return query
|
|
619
|
+
select v_message.id, v_recipient.id, v_inbox.id, v_callback_count, v_message.created_at;
|
|
620
|
+
end;
|
|
621
|
+
$$;
|
|
622
|
+
|
|
623
|
+
create or replace function agent_board.send_multicast_message(
|
|
624
|
+
p_thread_id uuid,
|
|
625
|
+
p_author_agent_id uuid,
|
|
626
|
+
p_recipient_agent_ids uuid[],
|
|
627
|
+
p_subject text,
|
|
628
|
+
p_body text,
|
|
629
|
+
p_message_type text default 'comment',
|
|
630
|
+
p_priority text default 'medium',
|
|
631
|
+
p_idempotency_key text default null,
|
|
632
|
+
p_metadata jsonb default '{}'::jsonb
|
|
633
|
+
)
|
|
634
|
+
returns table(message_id uuid, delivered_recipient_count integer, inbox_item_count integer, callback_outbox_count integer, created_at timestamptz)
|
|
635
|
+
language plpgsql
|
|
636
|
+
security definer
|
|
637
|
+
set search_path = agent_board, public
|
|
638
|
+
as $$
|
|
639
|
+
declare
|
|
640
|
+
v_message agent_board.messages%rowtype;
|
|
641
|
+
v_created_message record;
|
|
642
|
+
v_recipient_ids uuid[];
|
|
643
|
+
v_recipient_id uuid;
|
|
644
|
+
v_recipient agent_board.delivery_recipients%rowtype;
|
|
645
|
+
v_inbox agent_board.inbox_items%rowtype;
|
|
646
|
+
v_delivered_count integer := 0;
|
|
647
|
+
v_inbox_count integer := 0;
|
|
648
|
+
v_callback_count integer := 0;
|
|
649
|
+
v_metadata jsonb;
|
|
650
|
+
v_preview text;
|
|
651
|
+
v_fetch_descriptor jsonb;
|
|
652
|
+
v_idempotency_key text := nullif(trim(p_idempotency_key), '');
|
|
653
|
+
begin
|
|
654
|
+
if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
|
|
655
|
+
raise exception 'subject and body are required' using errcode = '22023';
|
|
656
|
+
end if;
|
|
657
|
+
if p_priority not in ('low', 'medium', 'high', 'urgent') then
|
|
658
|
+
raise exception 'invalid priority' using errcode = '22023';
|
|
659
|
+
end if;
|
|
660
|
+
if p_recipient_agent_ids is null or cardinality(p_recipient_agent_ids) = 0 then
|
|
661
|
+
raise exception 'recipient_agent_ids must not be empty' using errcode = '22023';
|
|
662
|
+
end if;
|
|
663
|
+
if array_position(p_recipient_agent_ids, null) is not null then
|
|
664
|
+
raise exception 'recipient_agent_ids must not contain nulls' using errcode = '22023';
|
|
665
|
+
end if;
|
|
666
|
+
if not exists (select 1 from agent_board.threads where id = p_thread_id) then
|
|
667
|
+
raise exception 'thread not found' using errcode = 'P0002';
|
|
668
|
+
end if;
|
|
669
|
+
if not exists (select 1 from agent_board.agents where id = p_author_agent_id) then
|
|
670
|
+
raise exception 'author agent not found' using errcode = 'P0002';
|
|
671
|
+
end if;
|
|
672
|
+
|
|
673
|
+
select array_agg(distinct recipient_id)
|
|
674
|
+
into v_recipient_ids
|
|
675
|
+
from unnest(p_recipient_agent_ids) as recipient_id;
|
|
676
|
+
|
|
677
|
+
if (
|
|
678
|
+
select count(*)
|
|
679
|
+
from agent_board.agents
|
|
680
|
+
where id = any(v_recipient_ids)
|
|
681
|
+
) <> cardinality(v_recipient_ids) then
|
|
682
|
+
raise exception 'recipient agent not found' using errcode = 'P0002';
|
|
683
|
+
end if;
|
|
684
|
+
|
|
685
|
+
perform agent_board._lock_idempotency_key(
|
|
686
|
+
'send_multicast_message:' || p_thread_id::text || ':' || p_author_agent_id::text,
|
|
687
|
+
v_idempotency_key
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
if v_idempotency_key is not null then
|
|
691
|
+
select *
|
|
692
|
+
into v_message
|
|
693
|
+
from agent_board.messages
|
|
694
|
+
where thread_id = p_thread_id
|
|
695
|
+
and author_agent_id = p_author_agent_id
|
|
696
|
+
and metadata ->> 'rpcName' = 'send_multicast_message'
|
|
697
|
+
and metadata ->> 'idempotencyKey' = v_idempotency_key
|
|
698
|
+
order by created_at desc
|
|
699
|
+
limit 1;
|
|
700
|
+
end if;
|
|
701
|
+
|
|
702
|
+
if v_message.id is null then
|
|
703
|
+
v_metadata := coalesce(p_metadata, '{}'::jsonb)
|
|
704
|
+
|| jsonb_build_object(
|
|
705
|
+
'rpcName', 'send_multicast_message',
|
|
706
|
+
'subject', trim(p_subject),
|
|
707
|
+
'priority', p_priority
|
|
708
|
+
);
|
|
709
|
+
if v_idempotency_key is not null then
|
|
710
|
+
v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
|
|
711
|
+
end if;
|
|
712
|
+
|
|
713
|
+
select *
|
|
714
|
+
into v_created_message
|
|
715
|
+
from agent_board.post_message(
|
|
716
|
+
p_thread_id,
|
|
717
|
+
p_author_agent_id,
|
|
718
|
+
p_body,
|
|
719
|
+
p_message_type,
|
|
720
|
+
null,
|
|
721
|
+
v_metadata
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
select *
|
|
725
|
+
into v_message
|
|
726
|
+
from agent_board.messages
|
|
727
|
+
where id = v_created_message.message_id;
|
|
728
|
+
end if;
|
|
729
|
+
|
|
730
|
+
v_preview := agent_board._trim_inbox_text(p_body, 280);
|
|
731
|
+
|
|
732
|
+
foreach v_recipient_id in array v_recipient_ids loop
|
|
733
|
+
v_fetch_descriptor := agent_board._build_fetch_descriptor(
|
|
734
|
+
'thread',
|
|
735
|
+
p_thread_id,
|
|
736
|
+
'agent_board.v_thread_feed',
|
|
737
|
+
'multicast delivery',
|
|
738
|
+
jsonb_build_object('messageId', v_message.id::text, 'recipientAgentId', v_recipient_id::text)
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
v_recipient := agent_board._upsert_delivery_recipient(
|
|
742
|
+
'multicast',
|
|
743
|
+
v_recipient_id,
|
|
744
|
+
'message',
|
|
745
|
+
v_message.id,
|
|
746
|
+
null,
|
|
747
|
+
null,
|
|
748
|
+
null,
|
|
749
|
+
null,
|
|
750
|
+
jsonb_build_object('priority', p_priority)
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
v_inbox := agent_board._upsert_inbox_item(
|
|
754
|
+
v_recipient.id,
|
|
755
|
+
v_recipient_id,
|
|
756
|
+
null,
|
|
757
|
+
v_recipient.delivery_mode,
|
|
758
|
+
'message',
|
|
759
|
+
v_message.id,
|
|
760
|
+
null,
|
|
761
|
+
null,
|
|
762
|
+
trim(p_subject),
|
|
763
|
+
v_preview,
|
|
764
|
+
p_priority,
|
|
765
|
+
true,
|
|
766
|
+
false,
|
|
767
|
+
v_fetch_descriptor,
|
|
768
|
+
jsonb_build_object('threadId', p_thread_id::text)
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
v_delivered_count := v_delivered_count + 1;
|
|
772
|
+
v_inbox_count := v_inbox_count + 1;
|
|
773
|
+
v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
|
|
774
|
+
end loop;
|
|
775
|
+
|
|
776
|
+
return query
|
|
777
|
+
select v_message.id, v_delivered_count, v_inbox_count, v_callback_count, v_message.created_at;
|
|
778
|
+
end;
|
|
779
|
+
$$;
|
|
780
|
+
|
|
781
|
+
create or replace function agent_board.post_board_message(
|
|
782
|
+
p_thread_id uuid,
|
|
783
|
+
p_author_agent_id uuid,
|
|
784
|
+
p_board_id uuid,
|
|
785
|
+
p_subject text,
|
|
786
|
+
p_body text,
|
|
787
|
+
p_message_type text default 'comment',
|
|
788
|
+
p_priority text default 'medium',
|
|
789
|
+
p_idempotency_key text default null,
|
|
790
|
+
p_metadata jsonb default '{}'::jsonb
|
|
791
|
+
)
|
|
792
|
+
returns table(message_id uuid, board_id uuid, delivered_recipient_count integer, callback_outbox_count integer, created_at timestamptz)
|
|
793
|
+
language plpgsql
|
|
794
|
+
security definer
|
|
795
|
+
set search_path = agent_board, public
|
|
796
|
+
as $$
|
|
797
|
+
declare
|
|
798
|
+
v_message agent_board.messages%rowtype;
|
|
799
|
+
v_created_message record;
|
|
800
|
+
v_membership record;
|
|
801
|
+
v_recipient agent_board.delivery_recipients%rowtype;
|
|
802
|
+
v_inbox agent_board.inbox_items%rowtype;
|
|
803
|
+
v_delivered_count integer := 0;
|
|
804
|
+
v_callback_count integer := 0;
|
|
805
|
+
v_metadata jsonb;
|
|
806
|
+
v_preview text;
|
|
807
|
+
v_fetch_descriptor jsonb;
|
|
808
|
+
v_idempotency_key text := nullif(trim(p_idempotency_key), '');
|
|
809
|
+
begin
|
|
810
|
+
if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
|
|
811
|
+
raise exception 'subject and body are required' using errcode = '22023';
|
|
812
|
+
end if;
|
|
813
|
+
if p_priority not in ('low', 'medium', 'high', 'urgent') then
|
|
814
|
+
raise exception 'invalid priority' using errcode = '22023';
|
|
815
|
+
end if;
|
|
816
|
+
if not exists (select 1 from agent_board.threads where id = p_thread_id) then
|
|
817
|
+
raise exception 'thread not found' using errcode = 'P0002';
|
|
818
|
+
end if;
|
|
819
|
+
if not exists (select 1 from agent_board.agents where id = p_author_agent_id) then
|
|
820
|
+
raise exception 'author agent not found' using errcode = 'P0002';
|
|
821
|
+
end if;
|
|
822
|
+
if not exists (select 1 from agent_board.boards where id = p_board_id) then
|
|
823
|
+
raise exception 'board not found' using errcode = 'P0002';
|
|
824
|
+
end if;
|
|
825
|
+
|
|
826
|
+
perform agent_board._lock_idempotency_key(
|
|
827
|
+
'post_board_message:' || p_thread_id::text || ':' || p_author_agent_id::text || ':' || p_board_id::text,
|
|
828
|
+
v_idempotency_key
|
|
829
|
+
);
|
|
830
|
+
|
|
831
|
+
if v_idempotency_key is not null then
|
|
832
|
+
select *
|
|
833
|
+
into v_message
|
|
834
|
+
from agent_board.messages
|
|
835
|
+
where thread_id = p_thread_id
|
|
836
|
+
and author_agent_id = p_author_agent_id
|
|
837
|
+
and metadata ->> 'rpcName' = 'post_board_message'
|
|
838
|
+
and metadata ->> 'idempotencyKey' = v_idempotency_key
|
|
839
|
+
order by created_at desc
|
|
840
|
+
limit 1;
|
|
841
|
+
end if;
|
|
842
|
+
|
|
843
|
+
if v_message.id is null then
|
|
844
|
+
v_metadata := coalesce(p_metadata, '{}'::jsonb)
|
|
845
|
+
|| jsonb_build_object(
|
|
846
|
+
'rpcName', 'post_board_message',
|
|
847
|
+
'subject', trim(p_subject),
|
|
848
|
+
'priority', p_priority,
|
|
849
|
+
'boardId', p_board_id::text
|
|
850
|
+
);
|
|
851
|
+
if v_idempotency_key is not null then
|
|
852
|
+
v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
|
|
853
|
+
end if;
|
|
854
|
+
|
|
855
|
+
select *
|
|
856
|
+
into v_created_message
|
|
857
|
+
from agent_board.post_message(
|
|
858
|
+
p_thread_id,
|
|
859
|
+
p_author_agent_id,
|
|
860
|
+
p_body,
|
|
861
|
+
p_message_type,
|
|
862
|
+
null,
|
|
863
|
+
v_metadata
|
|
864
|
+
);
|
|
865
|
+
|
|
866
|
+
select *
|
|
867
|
+
into v_message
|
|
868
|
+
from agent_board.messages
|
|
869
|
+
where id = v_created_message.message_id;
|
|
870
|
+
end if;
|
|
871
|
+
|
|
872
|
+
v_preview := agent_board._trim_inbox_text(p_body, 280);
|
|
873
|
+
v_fetch_descriptor := agent_board._build_fetch_descriptor(
|
|
874
|
+
'board',
|
|
875
|
+
p_board_id,
|
|
876
|
+
'agent_board.v_board_feed',
|
|
877
|
+
'board delivery',
|
|
878
|
+
jsonb_build_object('messageId', v_message.id::text)
|
|
879
|
+
);
|
|
880
|
+
|
|
881
|
+
for v_membership in
|
|
882
|
+
select bm.id, bm.agent_id
|
|
883
|
+
from agent_board.board_memberships bm
|
|
884
|
+
where bm.board_id = p_board_id
|
|
885
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
886
|
+
order by bm.created_at
|
|
887
|
+
loop
|
|
888
|
+
v_recipient := agent_board._upsert_delivery_recipient(
|
|
889
|
+
'board',
|
|
890
|
+
v_membership.agent_id,
|
|
891
|
+
'message',
|
|
892
|
+
v_message.id,
|
|
893
|
+
null,
|
|
894
|
+
null,
|
|
895
|
+
p_board_id,
|
|
896
|
+
v_membership.id,
|
|
897
|
+
jsonb_build_object('priority', p_priority)
|
|
898
|
+
);
|
|
899
|
+
|
|
900
|
+
v_inbox := agent_board._upsert_inbox_item(
|
|
901
|
+
v_recipient.id,
|
|
902
|
+
v_membership.agent_id,
|
|
903
|
+
p_board_id,
|
|
904
|
+
v_recipient.delivery_mode,
|
|
905
|
+
'message',
|
|
906
|
+
v_message.id,
|
|
907
|
+
null,
|
|
908
|
+
null,
|
|
909
|
+
trim(p_subject),
|
|
910
|
+
v_preview,
|
|
911
|
+
p_priority,
|
|
912
|
+
true,
|
|
913
|
+
true,
|
|
914
|
+
v_fetch_descriptor,
|
|
915
|
+
jsonb_build_object('threadId', p_thread_id::text, 'boardId', p_board_id::text)
|
|
916
|
+
);
|
|
917
|
+
|
|
918
|
+
v_delivered_count := v_delivered_count + 1;
|
|
919
|
+
v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
|
|
920
|
+
end loop;
|
|
921
|
+
|
|
922
|
+
v_callback_count := v_callback_count + agent_board._queue_board_post_callbacks(
|
|
923
|
+
p_board_id,
|
|
924
|
+
v_message.id,
|
|
925
|
+
trim(p_subject),
|
|
926
|
+
v_preview,
|
|
927
|
+
p_priority,
|
|
928
|
+
v_fetch_descriptor
|
|
929
|
+
);
|
|
930
|
+
|
|
931
|
+
return query
|
|
932
|
+
select v_message.id, p_board_id, v_delivered_count, v_callback_count, v_message.created_at;
|
|
933
|
+
end;
|
|
934
|
+
$$;
|
|
935
|
+
|
|
936
|
+
create or replace function agent_board.create_tasking_delivery(
|
|
937
|
+
p_thread_id uuid,
|
|
938
|
+
p_created_by_agent_id uuid,
|
|
939
|
+
p_title text,
|
|
940
|
+
p_details text default null,
|
|
941
|
+
p_recipient_agent_ids uuid[] default null,
|
|
942
|
+
p_board_id uuid default null,
|
|
943
|
+
p_priority text default 'medium',
|
|
944
|
+
p_orchestration_mode text default 'distributed',
|
|
945
|
+
p_idempotency_key text default null,
|
|
946
|
+
p_metadata jsonb default '{}'::jsonb
|
|
947
|
+
)
|
|
948
|
+
returns table(task_id uuid, created_event_id uuid, delivered_recipient_count integer, inbox_item_count integer, callback_outbox_count integer, created_at timestamptz, updated_at timestamptz)
|
|
949
|
+
language plpgsql
|
|
950
|
+
security definer
|
|
951
|
+
set search_path = agent_board, public
|
|
952
|
+
as $$
|
|
953
|
+
declare
|
|
954
|
+
v_task agent_board.tasks%rowtype;
|
|
955
|
+
v_created_event_id uuid;
|
|
956
|
+
v_created_task record;
|
|
957
|
+
v_recipient_ids uuid[];
|
|
958
|
+
v_recipient_id uuid;
|
|
959
|
+
v_membership record;
|
|
960
|
+
v_recipient agent_board.delivery_recipients%rowtype;
|
|
961
|
+
v_inbox agent_board.inbox_items%rowtype;
|
|
962
|
+
v_delivered_count integer := 0;
|
|
963
|
+
v_inbox_count integer := 0;
|
|
964
|
+
v_callback_count integer := 0;
|
|
965
|
+
v_metadata jsonb;
|
|
966
|
+
v_preview text;
|
|
967
|
+
v_fetch_descriptor jsonb;
|
|
968
|
+
v_idempotency_key text := nullif(trim(p_idempotency_key), '');
|
|
969
|
+
begin
|
|
970
|
+
if nullif(trim(p_title), '') is null then
|
|
971
|
+
raise exception 'task title is required' using errcode = '22023';
|
|
972
|
+
end if;
|
|
973
|
+
if p_priority not in ('low', 'medium', 'high', 'urgent') then
|
|
974
|
+
raise exception 'invalid priority' using errcode = '22023';
|
|
975
|
+
end if;
|
|
976
|
+
if p_orchestration_mode not in ('distributed', 'hierarchical', 'hybrid') then
|
|
977
|
+
raise exception 'invalid orchestration mode' using errcode = '22023';
|
|
978
|
+
end if;
|
|
979
|
+
if not exists (select 1 from agent_board.threads where id = p_thread_id) then
|
|
980
|
+
raise exception 'thread not found' using errcode = 'P0002';
|
|
981
|
+
end if;
|
|
982
|
+
if not exists (select 1 from agent_board.agents where id = p_created_by_agent_id) then
|
|
983
|
+
raise exception 'creator agent not found' using errcode = 'P0002';
|
|
984
|
+
end if;
|
|
985
|
+
if (p_recipient_agent_ids is null or cardinality(p_recipient_agent_ids) = 0) and p_board_id is null then
|
|
986
|
+
raise exception 'tasking delivery requires explicit recipients or a board target' using errcode = '22023';
|
|
987
|
+
end if;
|
|
988
|
+
if p_recipient_agent_ids is not null and array_position(p_recipient_agent_ids, null) is not null then
|
|
989
|
+
raise exception 'recipient_agent_ids must not contain nulls' using errcode = '22023';
|
|
990
|
+
end if;
|
|
991
|
+
if p_board_id is not null and not exists (select 1 from agent_board.boards where id = p_board_id) then
|
|
992
|
+
raise exception 'board not found' using errcode = 'P0002';
|
|
993
|
+
end if;
|
|
994
|
+
|
|
995
|
+
select array_agg(distinct recipient_id)
|
|
996
|
+
into v_recipient_ids
|
|
997
|
+
from unnest(coalesce(p_recipient_agent_ids, '{}'::uuid[])) as recipient_id;
|
|
998
|
+
|
|
999
|
+
if v_recipient_ids is not null and (
|
|
1000
|
+
select count(*)
|
|
1001
|
+
from agent_board.agents
|
|
1002
|
+
where id = any(v_recipient_ids)
|
|
1003
|
+
) <> cardinality(v_recipient_ids) then
|
|
1004
|
+
raise exception 'recipient agent not found' using errcode = 'P0002';
|
|
1005
|
+
end if;
|
|
1006
|
+
|
|
1007
|
+
perform agent_board._lock_idempotency_key(
|
|
1008
|
+
'create_tasking_delivery:' || p_thread_id::text || ':' || p_created_by_agent_id::text,
|
|
1009
|
+
v_idempotency_key
|
|
1010
|
+
);
|
|
1011
|
+
|
|
1012
|
+
if v_idempotency_key is not null then
|
|
1013
|
+
select *
|
|
1014
|
+
into v_task
|
|
1015
|
+
from agent_board.tasks
|
|
1016
|
+
where thread_id = p_thread_id
|
|
1017
|
+
and created_by_agent_id = p_created_by_agent_id
|
|
1018
|
+
and metadata ->> 'rpcName' = 'create_tasking_delivery'
|
|
1019
|
+
and metadata ->> 'idempotencyKey' = v_idempotency_key
|
|
1020
|
+
order by created_at desc
|
|
1021
|
+
limit 1;
|
|
1022
|
+
end if;
|
|
1023
|
+
|
|
1024
|
+
if v_task.id is null then
|
|
1025
|
+
v_metadata := coalesce(p_metadata, '{}'::jsonb)
|
|
1026
|
+
|| jsonb_build_object(
|
|
1027
|
+
'rpcName', 'create_tasking_delivery',
|
|
1028
|
+
'priority', p_priority
|
|
1029
|
+
);
|
|
1030
|
+
if p_board_id is not null then
|
|
1031
|
+
v_metadata := v_metadata || jsonb_build_object('boardId', p_board_id::text);
|
|
1032
|
+
end if;
|
|
1033
|
+
if v_idempotency_key is not null then
|
|
1034
|
+
v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
|
|
1035
|
+
end if;
|
|
1036
|
+
|
|
1037
|
+
select *
|
|
1038
|
+
into v_created_task
|
|
1039
|
+
from agent_board.create_task(
|
|
1040
|
+
p_thread_id,
|
|
1041
|
+
p_created_by_agent_id,
|
|
1042
|
+
trim(p_title),
|
|
1043
|
+
p_details,
|
|
1044
|
+
null,
|
|
1045
|
+
null,
|
|
1046
|
+
p_priority,
|
|
1047
|
+
p_orchestration_mode,
|
|
1048
|
+
v_metadata
|
|
1049
|
+
);
|
|
1050
|
+
|
|
1051
|
+
v_created_event_id := v_created_task.created_event_id;
|
|
1052
|
+
|
|
1053
|
+
select *
|
|
1054
|
+
into v_task
|
|
1055
|
+
from agent_board.tasks
|
|
1056
|
+
where id = v_created_task.task_id;
|
|
1057
|
+
else
|
|
1058
|
+
select id
|
|
1059
|
+
into v_created_event_id
|
|
1060
|
+
from agent_board.task_events
|
|
1061
|
+
where task_id = v_task.id
|
|
1062
|
+
and event_sequence = 1;
|
|
1063
|
+
end if;
|
|
1064
|
+
|
|
1065
|
+
v_preview := agent_board._trim_inbox_text(coalesce(p_details, p_title), 280);
|
|
1066
|
+
v_fetch_descriptor := agent_board._build_fetch_descriptor(
|
|
1067
|
+
'task',
|
|
1068
|
+
v_task.id,
|
|
1069
|
+
'agent_board.v_task_tree',
|
|
1070
|
+
'tasking delivery',
|
|
1071
|
+
jsonb_build_object('threadId', p_thread_id::text)
|
|
1072
|
+
);
|
|
1073
|
+
|
|
1074
|
+
if v_recipient_ids is not null then
|
|
1075
|
+
foreach v_recipient_id in array v_recipient_ids loop
|
|
1076
|
+
v_recipient := agent_board._upsert_delivery_recipient(
|
|
1077
|
+
'tasking',
|
|
1078
|
+
v_recipient_id,
|
|
1079
|
+
'task',
|
|
1080
|
+
null,
|
|
1081
|
+
v_task.id,
|
|
1082
|
+
null,
|
|
1083
|
+
null,
|
|
1084
|
+
null,
|
|
1085
|
+
jsonb_build_object('priority', p_priority)
|
|
1086
|
+
);
|
|
1087
|
+
|
|
1088
|
+
v_inbox := agent_board._upsert_inbox_item(
|
|
1089
|
+
v_recipient.id,
|
|
1090
|
+
v_recipient_id,
|
|
1091
|
+
null,
|
|
1092
|
+
v_recipient.delivery_mode,
|
|
1093
|
+
'task',
|
|
1094
|
+
null,
|
|
1095
|
+
v_task.id,
|
|
1096
|
+
null,
|
|
1097
|
+
trim(p_title),
|
|
1098
|
+
v_preview,
|
|
1099
|
+
p_priority,
|
|
1100
|
+
true,
|
|
1101
|
+
false,
|
|
1102
|
+
v_fetch_descriptor,
|
|
1103
|
+
jsonb_build_object('threadId', p_thread_id::text)
|
|
1104
|
+
);
|
|
1105
|
+
|
|
1106
|
+
v_delivered_count := v_delivered_count + 1;
|
|
1107
|
+
v_inbox_count := v_inbox_count + 1;
|
|
1108
|
+
v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
|
|
1109
|
+
end loop;
|
|
1110
|
+
end if;
|
|
1111
|
+
|
|
1112
|
+
if p_board_id is not null then
|
|
1113
|
+
for v_membership in
|
|
1114
|
+
select bm.id, bm.agent_id
|
|
1115
|
+
from agent_board.board_memberships bm
|
|
1116
|
+
where bm.board_id = p_board_id
|
|
1117
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
1118
|
+
order by bm.created_at
|
|
1119
|
+
loop
|
|
1120
|
+
v_recipient := agent_board._upsert_delivery_recipient(
|
|
1121
|
+
'tasking',
|
|
1122
|
+
v_membership.agent_id,
|
|
1123
|
+
'task',
|
|
1124
|
+
null,
|
|
1125
|
+
v_task.id,
|
|
1126
|
+
null,
|
|
1127
|
+
p_board_id,
|
|
1128
|
+
v_membership.id,
|
|
1129
|
+
jsonb_build_object('priority', p_priority)
|
|
1130
|
+
);
|
|
1131
|
+
|
|
1132
|
+
v_inbox := agent_board._upsert_inbox_item(
|
|
1133
|
+
v_recipient.id,
|
|
1134
|
+
v_membership.agent_id,
|
|
1135
|
+
v_recipient.board_id,
|
|
1136
|
+
v_recipient.delivery_mode,
|
|
1137
|
+
'task',
|
|
1138
|
+
null,
|
|
1139
|
+
v_task.id,
|
|
1140
|
+
null,
|
|
1141
|
+
trim(p_title),
|
|
1142
|
+
v_preview,
|
|
1143
|
+
p_priority,
|
|
1144
|
+
true,
|
|
1145
|
+
v_recipient.board_id is not null,
|
|
1146
|
+
v_fetch_descriptor,
|
|
1147
|
+
jsonb_build_object('threadId', p_thread_id::text, 'boardId', p_board_id::text)
|
|
1148
|
+
);
|
|
1149
|
+
|
|
1150
|
+
v_delivered_count := v_delivered_count + 1;
|
|
1151
|
+
v_inbox_count := v_inbox_count + 1;
|
|
1152
|
+
v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
|
|
1153
|
+
end loop;
|
|
1154
|
+
end if;
|
|
1155
|
+
|
|
1156
|
+
return query
|
|
1157
|
+
select v_task.id, v_created_event_id, v_delivered_count, v_inbox_count, v_callback_count, v_task.created_at, v_task.updated_at;
|
|
1158
|
+
end;
|
|
1159
|
+
$$;
|
|
1160
|
+
|
|
1161
|
+
create or replace function agent_board.update_inbox_item_state(
|
|
1162
|
+
p_inbox_item_id uuid,
|
|
1163
|
+
p_recipient_agent_id uuid,
|
|
1164
|
+
p_action_state text,
|
|
1165
|
+
p_metadata jsonb default '{}'::jsonb
|
|
1166
|
+
)
|
|
1167
|
+
returns table(inbox_item_id uuid, recipient_agent_id uuid, action_state text, callback_outbox_count integer, updated_at timestamptz)
|
|
1168
|
+
language plpgsql
|
|
1169
|
+
security definer
|
|
1170
|
+
set search_path = agent_board, public
|
|
1171
|
+
as $$
|
|
1172
|
+
declare
|
|
1173
|
+
v_inbox agent_board.inbox_items%rowtype;
|
|
1174
|
+
v_callback_count integer := 0;
|
|
1175
|
+
begin
|
|
1176
|
+
if p_action_state not in ('new', 'read', 'acknowledged', 'deferred', 'dismissed', 'restored') then
|
|
1177
|
+
raise exception 'invalid inbox action state' using errcode = '22023';
|
|
1178
|
+
end if;
|
|
1179
|
+
|
|
1180
|
+
update agent_board.inbox_items ii
|
|
1181
|
+
set action_state = p_action_state,
|
|
1182
|
+
metadata = coalesce(ii.metadata, '{}'::jsonb) || coalesce(p_metadata, '{}'::jsonb),
|
|
1183
|
+
updated_at = now()
|
|
1184
|
+
where ii.id = p_inbox_item_id
|
|
1185
|
+
and ii.recipient_agent_id = p_recipient_agent_id
|
|
1186
|
+
returning * into v_inbox;
|
|
1187
|
+
|
|
1188
|
+
if v_inbox.id is null then
|
|
1189
|
+
raise exception 'inbox item not found' using errcode = 'P0002';
|
|
1190
|
+
end if;
|
|
1191
|
+
|
|
1192
|
+
v_callback_count := agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.updated');
|
|
1193
|
+
|
|
1194
|
+
return query
|
|
1195
|
+
select v_inbox.id, v_inbox.recipient_agent_id, v_inbox.action_state, v_callback_count, v_inbox.updated_at;
|
|
1196
|
+
end;
|
|
1197
|
+
$$;
|
|
1198
|
+
|
|
1199
|
+
revoke all on all functions in schema agent_board from public, anon, authenticated;
|
|
1200
|
+
grant execute on all functions in schema agent_board to service_role;
|