slotlock 0.8.0 → 0.9.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/CHANGELOG.md +237 -0
- package/agent-server.d.ts +25 -5
- package/agent-server.js +152 -7
- package/agent-store-backend.d.ts +6 -1
- package/agent-store-backend.js +861 -55
- package/application-api.d.ts +19 -1
- package/application-api.js +578 -21
- package/approvals.d.ts +2 -0
- package/approvals.js +12 -5
- package/assistant.d.ts +34 -8
- package/assistant.js +81 -26
- package/availability-store.d.ts +193 -0
- package/availability-store.js +558 -0
- package/booking-rules.d.ts +92 -5
- package/booking-rules.js +243 -15
- package/dashboard-assistant-ui.d.ts +1 -1
- package/dashboard-assistant-ui.js +109 -8
- package/dashboard.d.ts +55 -6
- package/dashboard.js +790 -105
- package/ddl.d.ts +5 -1
- package/ddl.js +760 -9
- package/event-feed.d.ts +1 -1
- package/event-feed.js +18 -0
- package/holiday-import.d.ts +38 -0
- package/holiday-import.js +57 -0
- package/holidays.d.ts +38 -0
- package/holidays.js +150 -0
- package/hours.d.ts +125 -0
- package/hours.js +224 -0
- package/index.d.ts +14 -3
- package/index.js +10 -2
- package/interop.d.ts +25 -7
- package/interop.js +145 -17
- package/lifecycle.d.ts +31 -0
- package/lifecycle.js +115 -0
- package/maintenance.d.ts +19 -0
- package/maintenance.js +55 -1
- package/metrics.js +3 -0
- package/openapi.json +16237 -7795
- package/package.json +1 -1
- package/ranges.d.ts +28 -0
- package/ranges.js +135 -0
- package/resource-search.d.ts +19 -0
- package/resource-search.js +153 -0
- package/rest-api.js +7 -0
- package/rules.d.ts +2 -0
- package/rules.js +2 -0
- package/self-host.d.ts +1 -1
- package/self-host.js +221 -21
- package/slotlock.manifest.json +1 -1
- package/store.d.ts +367 -24
- package/store.js +1567 -97
- package/types.d.ts +198 -0
package/agent-server.js
CHANGED
|
@@ -61,6 +61,8 @@ export const SLOTLOCK_A2A_TASK_STATES = Object.freeze([
|
|
|
61
61
|
]);
|
|
62
62
|
const DATE_TIME = z.string().datetime({ offset: true });
|
|
63
63
|
const RESOURCE_ID = z.string().trim().min(1).max(200);
|
|
64
|
+
/** The longest resource search (resource-search.ts SLOTLOCK_RESOURCE_SEARCH_MAX; a test holds them equal). */
|
|
65
|
+
export const SLOTLOCK_AGENT_RESOURCE_SEARCH_MAX = 200;
|
|
64
66
|
const EVENT_ID = z.string().trim().min(1).max(500);
|
|
65
67
|
const HOLD_ID = z.string().uuid();
|
|
66
68
|
const POOL = z.string().regex(/^[a-z0-9][a-z0-9._-]{0,99}$/);
|
|
@@ -112,6 +114,18 @@ function bookPool(backend, context, input) {
|
|
|
112
114
|
throw new SlotlockAgentOperationError('pools_unavailable', 409);
|
|
113
115
|
return backend.bookPool(context, input);
|
|
114
116
|
}
|
|
117
|
+
/** `slotlock_hold_together`, from a backend that holds sets (RFC 0004 B7). */
|
|
118
|
+
function holdTogether(backend, context, input) {
|
|
119
|
+
if (!backend.holdTogether)
|
|
120
|
+
throw new SlotlockAgentOperationError('holds_unavailable', 409);
|
|
121
|
+
return backend.holdTogether(context, input);
|
|
122
|
+
}
|
|
123
|
+
/** `slotlock_find_available_ranges`, from a backend that searches hires (RFC 0004 B3). */
|
|
124
|
+
function findAvailableRanges(backend, context, input) {
|
|
125
|
+
if (!backend.findAvailableRanges)
|
|
126
|
+
throw new SlotlockAgentOperationError('ranges_unavailable', 409);
|
|
127
|
+
return backend.findAvailableRanges(context, input);
|
|
128
|
+
}
|
|
115
129
|
/** `slotlock_get_pool_availability`, from a backend that knows pools, as `holdSlots`. */
|
|
116
130
|
function getPoolAvailability(backend, context, input) {
|
|
117
131
|
if (!backend.getPoolAvailability)
|
|
@@ -226,6 +240,8 @@ const ALTERNATIVE_REFUSALS = new Set([
|
|
|
226
240
|
'booking_rule_max_advance',
|
|
227
241
|
'booking_rule_slot_step',
|
|
228
242
|
'booking_rule_outside_hours',
|
|
243
|
+
'booking_rule_pickup_time',
|
|
244
|
+
'booking_rule_return_time',
|
|
229
245
|
]);
|
|
230
246
|
/** At most this many alternatives, the earliest first. */
|
|
231
247
|
export const SLOTLOCK_MAX_ALTERNATIVES = 3;
|
|
@@ -263,7 +279,8 @@ async function alternativesFor(backend, context, want) {
|
|
|
263
279
|
const found = await findAvailableSlots(backend, { ...context, operation: 'slotlock_find_available_slots' }, {
|
|
264
280
|
resource_ids: resourceIds,
|
|
265
281
|
start: new Date(from).toISOString(),
|
|
266
|
-
|
|
282
|
+
// Starts over the next seven days, however long the booking runs.
|
|
283
|
+
end: new Date(from + ALTERNATIVES_WINDOW_MS + durationMs).toISOString(),
|
|
267
284
|
duration_minutes: durationMs / 60_000,
|
|
268
285
|
limit: SLOTLOCK_MAX_ALTERNATIVES,
|
|
269
286
|
...(want.needed !== undefined ? { require_free: Math.min(want.needed, resourceIds.length) } : {}),
|
|
@@ -542,7 +559,9 @@ const OPERATION_SCOPES = Object.freeze({
|
|
|
542
559
|
slotlock_get_free_busy: 'read',
|
|
543
560
|
slotlock_find_next_available: 'read',
|
|
544
561
|
slotlock_find_available_slots: 'read',
|
|
562
|
+
slotlock_find_available_ranges: 'read',
|
|
545
563
|
slotlock_hold_slots: 'write',
|
|
564
|
+
slotlock_hold_together: 'write',
|
|
546
565
|
slotlock_book_together: 'write',
|
|
547
566
|
slotlock_book_pool: 'write',
|
|
548
567
|
slotlock_get_pool_availability: 'read',
|
|
@@ -614,6 +633,14 @@ const SELECTING_OPERATIONS = new Map([
|
|
|
614
633
|
['slotlock_find_available_slots', 20],
|
|
615
634
|
]);
|
|
616
635
|
const SELECTION_FIELDS = ['location', 'group', 'labels', 'kind', 'pool'];
|
|
636
|
+
/** Searches that take the resources' usual length when they name none. */
|
|
637
|
+
const DEFAULT_LENGTH_OPERATIONS = new Set([
|
|
638
|
+
'slotlock_find_next_available',
|
|
639
|
+
'slotlock_find_available_slots',
|
|
640
|
+
]);
|
|
641
|
+
/** The most ranges one search answers. */
|
|
642
|
+
const MAX_LISTED_RANGES = 50;
|
|
643
|
+
const MAX_RANGE_RESOURCES = 100;
|
|
617
644
|
/** Whether a search's input names its resources, or selects them, and not both or neither. */
|
|
618
645
|
function selectsOrNames(input) {
|
|
619
646
|
const selects = SELECTION_FIELDS.some((field) => input[field] !== undefined);
|
|
@@ -656,6 +683,8 @@ const EVENT_CREATE_INPUT = z
|
|
|
656
683
|
recurrence_exceptions: z.array(RECURRENCE_EXCEPTION_INPUT).max(500).optional(),
|
|
657
684
|
/** Book the time one of your holds keeps (slotlock_hold_slots); its alternatives are released. */
|
|
658
685
|
hold_id: HOLD_ID.optional(),
|
|
686
|
+
/** False: no one should be notified about it, nor reminded (RFC 0004 P3). Default true. */
|
|
687
|
+
notify: z.boolean().optional(),
|
|
659
688
|
})
|
|
660
689
|
.strict()
|
|
661
690
|
.refine((value) => boundedEventSpan(value.starts_at, value.ends_at));
|
|
@@ -663,10 +692,11 @@ const OPERATION_DEFINITIONS = [
|
|
|
663
692
|
{
|
|
664
693
|
name: 'slotlock_list_resources',
|
|
665
694
|
title: 'List calendar resources',
|
|
666
|
-
description: 'List resources visible to the authenticated tenant principal, with what each says about itself: its name (show it rather than the reference), description, kind, capacity and labels, where it is (location), the groups it is in (a team, a department), and the pool it belongs to (a vehicle class, a room type). With kind, only resources of that kind; with labels, only those with every label given; with location, only those there or at a location in it; with group, only that group\'s. With pool, only that pool\'s members, in the order slotlock_book_pool tries them.',
|
|
695
|
+
description: 'List resources visible to the authenticated tenant principal, with what each says about itself: its name (show it rather than the reference), description, kind, capacity and labels, where it is (location), the groups it is in (a team, a department), and the pool it belongs to (a vehicle class, a room type). With kind, only resources of that kind; with labels, only those with every label given; with location, only those there or at a location in it; with group, only that group\'s. With pool, only that pool\'s members, in the order slotlock_book_pool tries them. With query, the resources that best match what a person calls one, best first, at most limit and with no next_cursor: each word is looked for in the name, reference, labels, kind, location and description, ignoring case and accents, and forgiving a typo, a word cut short or words run together ("mercedez", "merc 2025" and "mercedesbenz" find "Mercedes-Benz 2025"); numbers match exactly. It is the way to find the id of a resource a person names, since other tools take ids only. When several match about equally, ask which one is meant. A resource that states no kind, labels or location is left out by those filters, so search without them before saying a resource does not exist.',
|
|
667
696
|
input: z
|
|
668
697
|
.object({
|
|
669
698
|
cursor: z.string().max(500).optional(),
|
|
699
|
+
query: z.string().trim().min(1).max(SLOTLOCK_AGENT_RESOURCE_SEARCH_MAX).optional(),
|
|
670
700
|
limit: z.number().int().min(1).max(100).default(50),
|
|
671
701
|
pool: POOL.optional(),
|
|
672
702
|
kind: RESOURCE_KIND.optional(),
|
|
@@ -758,7 +788,8 @@ const OPERATION_DEFINITIONS = [
|
|
|
758
788
|
pool: POOL.optional(),
|
|
759
789
|
start: DATE_TIME,
|
|
760
790
|
end: DATE_TIME,
|
|
761
|
-
|
|
791
|
+
/** Left out: the resources' usual length (their booking rules' default_duration_minutes). */
|
|
792
|
+
duration_minutes: z.number().int().min(1).max(MAX_SLOT_MINUTES).optional(),
|
|
762
793
|
})
|
|
763
794
|
.strict()
|
|
764
795
|
.refine(selectsOrNames, 'Give resource_ids, or select resources by location, group, labels, kind or pool: one or the other')
|
|
@@ -791,7 +822,8 @@ const OPERATION_DEFINITIONS = [
|
|
|
791
822
|
pool: POOL.optional(),
|
|
792
823
|
start: DATE_TIME,
|
|
793
824
|
end: DATE_TIME,
|
|
794
|
-
|
|
825
|
+
/** Left out: the resources' usual length (their booking rules' default_duration_minutes). */
|
|
826
|
+
duration_minutes: z.number().int().min(1).max(MAX_SLOT_MINUTES).optional(),
|
|
795
827
|
limit: z.number().int().min(1).max(MAX_LISTED_SLOTS).default(10),
|
|
796
828
|
require_free: z.number().int().min(1).max(MAX_SLOT_RESOURCES).optional(),
|
|
797
829
|
})
|
|
@@ -819,6 +851,54 @@ const OPERATION_DEFINITIONS = [
|
|
|
819
851
|
invoke: findAvailableSlots,
|
|
820
852
|
example: { resource_ids: [EXAMPLE_RESOURCE_ID], ...EXAMPLE_WINDOW, duration_minutes: 60, limit: 10 },
|
|
821
853
|
},
|
|
854
|
+
{
|
|
855
|
+
name: 'slotlock_find_available_ranges',
|
|
856
|
+
title: 'Find available ranges',
|
|
857
|
+
description: 'Find hires across days, such as a car from Friday to Monday: for each resource named, or each member of a pool, the earliest range that starts between pickup_start and pickup_end and either lasts `days` days (ending at the same local time) or ends between return_start and return_end. Each range is free throughout with its buffers, allowed by the resource\'s booking rules (pick-up and return times, lengths, notice, step) and starts and ends inside its bookable hours. Up to `limit` ranges, earliest first, one per resource; none unless every resource\'s coverage is certain. Book one with slotlock_create_event, or a pool\'s with slotlock_book_pool, at its start and end.',
|
|
858
|
+
input: z
|
|
859
|
+
.object({
|
|
860
|
+
resource_ids: z.array(RESOURCE_ID).min(1).max(MAX_SLOT_RESOURCES).optional(),
|
|
861
|
+
pool: POOL.optional(),
|
|
862
|
+
pickup_start: DATE_TIME,
|
|
863
|
+
pickup_end: DATE_TIME,
|
|
864
|
+
days: z.number().int().min(1).max(366).optional(),
|
|
865
|
+
return_start: DATE_TIME.optional(),
|
|
866
|
+
return_end: DATE_TIME.optional(),
|
|
867
|
+
limit: z.number().int().min(1).max(MAX_LISTED_RANGES).default(10),
|
|
868
|
+
})
|
|
869
|
+
.strict()
|
|
870
|
+
.refine((value) => (value.resource_ids !== undefined) !== (value.pool !== undefined), 'Give resource_ids or pool, one of them')
|
|
871
|
+
.refine((value) => value.days !== undefined
|
|
872
|
+
? value.return_start === undefined && value.return_end === undefined
|
|
873
|
+
: value.return_start !== undefined && value.return_end !== undefined, 'Give days, or return_start and return_end')
|
|
874
|
+
.refine((value) => {
|
|
875
|
+
const pickupStart = Date.parse(value.pickup_start);
|
|
876
|
+
const pickupEnd = Date.parse(value.pickup_end);
|
|
877
|
+
const last = value.days !== undefined ? pickupEnd + (value.days + 1) * 86_400_000 : Date.parse(value.return_end ?? '');
|
|
878
|
+
const returnStart = value.return_start === undefined ? pickupStart : Date.parse(value.return_start);
|
|
879
|
+
return pickupEnd >= pickupStart && last >= returnStart && last - pickupStart <= MAX_AGENT_WINDOW_MS;
|
|
880
|
+
}, 'The windows must run forward, within the longest window a search reads'),
|
|
881
|
+
output: z
|
|
882
|
+
.object({
|
|
883
|
+
ranges: z
|
|
884
|
+
.array(z.object({ resource_id: RESOURCE_ID, start: DATE_TIME, end: DATE_TIME }).strict())
|
|
885
|
+
.max(MAX_LISTED_RANGES),
|
|
886
|
+
coverage: COVERAGE,
|
|
887
|
+
/** The pool's members searched, when the call named a pool. */
|
|
888
|
+
selected_resource_ids: z.array(RESOURCE_ID).max(MAX_RANGE_RESOURCES).optional(),
|
|
889
|
+
})
|
|
890
|
+
.strict(),
|
|
891
|
+
risk: { readOnly: true, destructive: false, idempotent: true },
|
|
892
|
+
invoke: findAvailableRanges,
|
|
893
|
+
example: {
|
|
894
|
+
pool: 'compact-car',
|
|
895
|
+
pickup_start: '2027-03-05T09:00:00Z',
|
|
896
|
+
pickup_end: '2027-03-05T12:00:00Z',
|
|
897
|
+
return_start: '2027-03-08T09:00:00Z',
|
|
898
|
+
return_end: '2027-03-08T12:00:00Z',
|
|
899
|
+
limit: 5,
|
|
900
|
+
},
|
|
901
|
+
},
|
|
822
902
|
{
|
|
823
903
|
name: 'slotlock_create_event',
|
|
824
904
|
title: 'Create resource event',
|
|
@@ -890,7 +970,7 @@ const OPERATION_DEFINITIONS = [
|
|
|
890
970
|
{
|
|
891
971
|
name: 'slotlock_book_together',
|
|
892
972
|
title: 'Book resources together',
|
|
893
|
-
description: 'Book the same time on 2-20 resources at once, all or none: a van with its driver, a room with its projector. If any of them is taken, nothing is booked, and the refusal names that resource_id. Find such a time with slotlock_find_available_slots and require_free. A retry with the same idempotency_key answers the same events.',
|
|
973
|
+
description: 'Book the same time on 2-20 resources at once, all or none: a van with its driver, a room with its projector. If any of them is taken, nothing is booked, and the refusal names that resource_id. Find such a time with slotlock_find_available_slots and require_free. With hold_group_id, it books the set slotlock_hold_together held, from those holds. A retry with the same idempotency_key answers the same events.',
|
|
894
974
|
input: z
|
|
895
975
|
.object({
|
|
896
976
|
resource_ids: z.array(RESOURCE_ID).min(2).max(MAX_SLOT_RESOURCES),
|
|
@@ -898,6 +978,8 @@ const OPERATION_DEFINITIONS = [
|
|
|
898
978
|
ends_at: DATE_TIME,
|
|
899
979
|
timezone: z.string().min(1).max(100),
|
|
900
980
|
title: z.string().trim().min(1).max(500).optional(),
|
|
981
|
+
/** The set slotlock_hold_together held, booked from its holds (RFC 0004 B7). */
|
|
982
|
+
hold_group_id: z.string().uuid().optional(),
|
|
901
983
|
idempotency_key: IDEMPOTENCY_KEY,
|
|
902
984
|
})
|
|
903
985
|
.strict()
|
|
@@ -1032,7 +1114,7 @@ const OPERATION_DEFINITIONS = [
|
|
|
1032
1114
|
{
|
|
1033
1115
|
name: 'slotlock_hold_slots',
|
|
1034
1116
|
title: 'Hold candidate slots',
|
|
1035
|
-
description: 'Hold up to five candidate slots for a short time (30-900 seconds) while a person chooses, as alternatives in one group: each free one is held, the others are listed with why not. Book one by passing its hold_id to slotlock_create_event, which releases the rest; unbooked holds expire on their own. A hold is yours alone, and you may have 20 live at once. A candidate refused for its time comes with alternatives (the earliest free times of its length). slotlock_confirm_slot books a held time.',
|
|
1117
|
+
description: 'Hold up to five candidate slots for a short time (30-900 seconds) while a person chooses, as alternatives in one group: each free one is held, the others are listed with why not. Book one by passing its hold_id to slotlock_create_event, which releases the rest; unbooked holds expire on their own. With payment_required, a hold is booked only once the system you book for records its payment (the person pays there): booking it before then answers payment_required, so send the person to pay and try again. A hold is yours alone, and you may have 20 live at once. A candidate refused for its time comes with alternatives (the earliest free times of its length). slotlock_confirm_slot books a held time.',
|
|
1036
1118
|
input: z
|
|
1037
1119
|
.object({
|
|
1038
1120
|
slots: z
|
|
@@ -1044,8 +1126,19 @@ const OPERATION_DEFINITIONS = [
|
|
|
1044
1126
|
.max(5),
|
|
1045
1127
|
ttl_seconds: z.number().int().min(30).max(900).default(300),
|
|
1046
1128
|
idempotency_key: IDEMPOTENCY_KEY,
|
|
1129
|
+
/** Booked only once the integrator records its payment (RFC 0004 C3). */
|
|
1130
|
+
payment_required: z.boolean().optional(),
|
|
1131
|
+
/** What that payment must be, in the currency's minor unit. */
|
|
1132
|
+
payment: z
|
|
1133
|
+
.object({ amount_minor: z.number().int().min(0).max(1_000_000_000_000), currency: z.string().regex(/^[A-Z]{3}$/) })
|
|
1134
|
+
.strict()
|
|
1135
|
+
.optional(),
|
|
1047
1136
|
})
|
|
1048
|
-
.strict()
|
|
1137
|
+
.strict()
|
|
1138
|
+
.refine((value) => value.payment === undefined || value.payment_required === true, {
|
|
1139
|
+
message: 'payment needs payment_required',
|
|
1140
|
+
path: ['payment'],
|
|
1141
|
+
}),
|
|
1049
1142
|
output: z
|
|
1050
1143
|
.object({
|
|
1051
1144
|
group_id: HOLD_ID,
|
|
@@ -1057,6 +1150,12 @@ const OPERATION_DEFINITIONS = [
|
|
|
1057
1150
|
start: DATE_TIME,
|
|
1058
1151
|
end: DATE_TIME,
|
|
1059
1152
|
expires_at: DATE_TIME,
|
|
1153
|
+
payment_required: z.literal(true).optional(),
|
|
1154
|
+
payment_amount: z
|
|
1155
|
+
.object({ amount_minor: z.number().int().min(0), currency: z.string().regex(/^[A-Z]{3}$/) })
|
|
1156
|
+
.strict()
|
|
1157
|
+
.nullable()
|
|
1158
|
+
.optional(),
|
|
1060
1159
|
})
|
|
1061
1160
|
.strict())
|
|
1062
1161
|
.max(5),
|
|
@@ -1083,6 +1182,42 @@ const OPERATION_DEFINITIONS = [
|
|
|
1083
1182
|
idempotency_key: 'hold-handover-2027-03-02',
|
|
1084
1183
|
},
|
|
1085
1184
|
},
|
|
1185
|
+
{
|
|
1186
|
+
name: 'slotlock_hold_together',
|
|
1187
|
+
title: 'Hold resources together',
|
|
1188
|
+
description: 'Hold one time on 2-20 resources at once, all or none, such as a car with its driver while a person decides: if any is taken, nothing is held and the refusal names that resource_id. The holds are one group with one time to live (ttl_seconds, the earliest of them). Book the set with slotlock_book_together and its hold_group_id; release them with slotlock_release_slots and any hold_id. A retry with the same idempotency_key answers the same holds.',
|
|
1189
|
+
input: z
|
|
1190
|
+
.object({
|
|
1191
|
+
resource_ids: z.array(RESOURCE_ID).min(2).max(MAX_SLOT_RESOURCES),
|
|
1192
|
+
start: DATE_TIME,
|
|
1193
|
+
end: DATE_TIME,
|
|
1194
|
+
ttl_seconds: z.number().int().min(30).max(900).default(300),
|
|
1195
|
+
idempotency_key: IDEMPOTENCY_KEY,
|
|
1196
|
+
})
|
|
1197
|
+
.strict()
|
|
1198
|
+
.refine(boundedAgentWindow),
|
|
1199
|
+
output: z
|
|
1200
|
+
.object({
|
|
1201
|
+
group_id: z.string().uuid(),
|
|
1202
|
+
holds: z
|
|
1203
|
+
.array(z
|
|
1204
|
+
.object({ hold_id: z.string().uuid(), resource_id: RESOURCE_ID, start: DATE_TIME, end: DATE_TIME, expires_at: DATE_TIME })
|
|
1205
|
+
.strict())
|
|
1206
|
+
.min(2)
|
|
1207
|
+
.max(MAX_SLOT_RESOURCES),
|
|
1208
|
+
expires_at: DATE_TIME,
|
|
1209
|
+
})
|
|
1210
|
+
.strict(),
|
|
1211
|
+
risk: { readOnly: false, destructive: false, idempotent: true },
|
|
1212
|
+
invoke: holdTogether,
|
|
1213
|
+
example: {
|
|
1214
|
+
resource_ids: [EXAMPLE_RESOURCE_ID, '3c2d1e0f-9a8b-4c7d-8e6f-5a4b3c2d1e0f'],
|
|
1215
|
+
start: '2027-03-02T09:00:00Z',
|
|
1216
|
+
end: '2027-03-02T17:00:00Z',
|
|
1217
|
+
ttl_seconds: 300,
|
|
1218
|
+
idempotency_key: 'hold-van-and-driver-1',
|
|
1219
|
+
},
|
|
1220
|
+
},
|
|
1086
1221
|
{
|
|
1087
1222
|
name: 'slotlock_release_slots',
|
|
1088
1223
|
title: 'Release held slots',
|
|
@@ -1633,6 +1768,15 @@ export async function invokeSlotlockAgentOperation(args) {
|
|
|
1633
1768
|
input = Object.fromEntries(Object.entries(input).filter(([key]) => !SELECTION_FIELDS.includes(key)));
|
|
1634
1769
|
input.resource_ids = selected;
|
|
1635
1770
|
}
|
|
1771
|
+
// A search that names no length takes the resources' usual one (RFC 0004 B3).
|
|
1772
|
+
if (DEFAULT_LENGTH_OPERATIONS.has(operation.name) && input.duration_minutes === undefined) {
|
|
1773
|
+
const usual = backend.defaultDuration
|
|
1774
|
+
? await backend.defaultDuration(context, (input.resource_ids ?? []))
|
|
1775
|
+
: null;
|
|
1776
|
+
if (usual === null)
|
|
1777
|
+
return { ok: false, status: 400, code: 'duration_required' };
|
|
1778
|
+
input = { ...input, duration_minutes: usual };
|
|
1779
|
+
}
|
|
1636
1780
|
const asked = input;
|
|
1637
1781
|
recover = (code) => alternativesForRefusal(backend, context, operation.name, asked, code);
|
|
1638
1782
|
let answered = await method.call(backend, context, input);
|
|
@@ -1990,6 +2134,7 @@ function parseSubscriptionFilter(value) {
|
|
|
1990
2134
|
/** Natural-language guidance `server/discover` gives the model on how to use these tools well. */
|
|
1991
2135
|
const SLOTLOCK_MCP_INSTRUCTIONS = [
|
|
1992
2136
|
'Slotlock is a tenant-isolated resource calendar (vehicles, rooms, equipment).',
|
|
2137
|
+
'A resource is named in calls by its id (resource_id, resource_ids), never by its name or reference: find the id of one a person names with slotlock_list_resources and query (what they call it; typos and accents are forgiven). resource_not_found for something that is not an id names, in candidate_1 to candidate_3, the resources that best match it. A kind, label or location filter leaves out resources that do not state it, so search without one before saying a resource does not exist, and ask the person when several match equally.',
|
|
1993
2138
|
'Check availability before booking: slotlock_get_free_busy returns busy intervals and slotlock_find_next_available the earliest free slot of a given length, and slotlock_find_available_slots up to 50 of them to offer a person a choice; an answer is certain only when coverage.certainty is "certain".',
|
|
1994
2139
|
'slotlock_create_event needs an idempotency_key: retry with the same key and arguments, never a new key for the same booking. slotlock_update_event and slotlock_delete_event need the event revision you last read as expected_revision.',
|
|
1995
2140
|
'Times are RFC 3339 with an offset; intervals are half-open [start, end).',
|
package/agent-store-backend.d.ts
CHANGED
|
@@ -39,7 +39,6 @@ export declare function slotlockAgentErrorStatus(code: string): 400 | 404 | 409
|
|
|
39
39
|
* store's client, used for the feed's head.
|
|
40
40
|
*/
|
|
41
41
|
export declare function createSlotlockStoreChangeSource(store: SlotlockStore, sql: Sql): SlotlockAgentChangeSource;
|
|
42
|
-
/** Bind the protocol server to the production store without giving a request control of tenancy. */
|
|
43
42
|
export declare function createSlotlockStoreAgentBackend(store: SlotlockStore, options: SlotlockStoreAgentBackendOptions): SlotlockAgentCalendarBackend;
|
|
44
43
|
export interface SlotlockStoreApplicationBackendOptions {
|
|
45
44
|
/**
|
|
@@ -66,6 +65,12 @@ export interface SlotlockStoreApplicationBackendOptions {
|
|
|
66
65
|
* approvals_unavailable; declining still works.
|
|
67
66
|
*/
|
|
68
67
|
agentBackend?: SlotlockAgentCalendarBackend;
|
|
68
|
+
/**
|
|
69
|
+
* How `slotlock_import_holidays` fetches a calendar's url (RFC 0004 B1): give it a fetch that
|
|
70
|
+
* refuses private addresses (`createSlotlockGuardedFetch`). Unset: a url answers 400
|
|
71
|
+
* holiday_url_not_served and only the calendar's text is read.
|
|
72
|
+
*/
|
|
73
|
+
holidayFetch?: typeof fetch;
|
|
69
74
|
}
|
|
70
75
|
/**
|
|
71
76
|
* Run a granted approval's write as the agent that asked for it, in one transaction with the grant:
|