@agentshouse/mdruntime-adapter 0.1.0-ceda7e87cc6e8 → 0.1.0-cedb8e57837a7

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/dist/site.js CHANGED
@@ -1,78 +1,150 @@
1
+ import { stepped } from '@agentshouse/mdruntime';
1
2
  import { actionOutcome, SITE_SUBMIT } from './actions.js';
2
3
  import { mapDiscovery, parseSiteDiscovery, siteCapabilities, siteTracked, } from './discovery.js';
3
- import { scopeModels, streamedModels, viewModels } from './model.js';
4
- import { advancedScope, bootstrapOf, parseEnumeration, parseSiteBootstrap, refreshFrom, viewOf, } from './read.js';
4
+ import { partModels, streamedModels, viewModels } from './model.js';
5
+ import { advancedScope, bootstrapping, parseEnumeration, parseSiteBootstrap, refreshFrom, siteBootstrapNext, siteBootstrapOffset, viewOf, wholeView, } from './read.js';
6
+ import { sha256Hex } from './digest.js';
5
7
  import { classifyDoor, classifyRead, discoveryFailure } from './refusal.js';
6
8
  import { houseNotifications } from './stream.js';
7
9
  import { SITE_DOOR_BOOTSTRAP, SITE_DOOR_DISCOVER, SITE_DOOR_ENUMERATE, SITE_DOOR_SUBMIT, SITE_FILE_RECORD, SITE_FILES, } from './transport.js';
8
10
  import { houseReferrer } from './referrer.js';
9
11
  import { followsVersion, logPosition, presented, reachedVersion, roomVersion, } from './version.js';
10
12
  const UNCERTAIN = { kind: 'transport-uncertainty' };
13
+ function unfound() {
14
+ return { kind: 'rejected', code: 'file_not_found', fields: {}, retryable: false };
15
+ }
11
16
  function siteRoom(tracked) {
12
17
  return {
13
18
  name: tracked.handle,
14
19
  locator: tracked.locator,
15
20
  handle: tracked.handle,
16
21
  roomVersion: roomVersion(tracked),
17
- operations: { read: [''], sync: [''], write: [], delete: [] },
18
- replica: { selection: tracked.locator, admission: 'admitted' },
22
+ operations: { write: [], delete: [] },
23
+ replica: { selection: tracked.locator, admission: 'admitted', offline: 'open' },
19
24
  providerOwnedPaths: [],
20
25
  };
21
26
  }
22
27
  function settled(refused) {
23
28
  return refused.kind === 'beyond-horizon' ? UNCERTAIN : refused;
24
29
  }
25
- export function createSiteAdapter(transport) {
30
+ export function createSiteAdapter(transport, snapshot = false) {
26
31
  const notifications = houseNotifications(transport, 'site');
27
32
  const tracked = new Map();
28
33
  function published(referrer) {
29
34
  const named = houseReferrer(referrer, tracked);
30
35
  return named === null || !tracked.has(named.locator) ? null : named;
31
36
  }
32
- async function enumeration(entry, from) {
37
+ function fileRoute(request) {
38
+ const named = published(request.kind === 'path' ? { room: request.locator, path: request.path } : request.referrer);
39
+ if (named === null) {
40
+ return { kind: 'rejected', code: 'referrer_unknown', fields: {}, retryable: false };
41
+ }
42
+ if (request.fileRef === undefined)
43
+ return unfound();
44
+ const { room_ref, path, record } = named.referrer;
45
+ return `${SITE_FILES}${room_ref}/${encodeURIComponent(request.fileRef)}/${path === undefined
46
+ ? `${SITE_FILE_RECORD}/${encodeURIComponent(record)}`
47
+ : encodeURIComponent(path)}`;
48
+ }
49
+ async function enumerated(entry, from, page) {
33
50
  const answer = await transport.door(SITE_DOOR_ENUMERATE, {
34
- positions: [presented(entry.authority, from)],
51
+ positions: [
52
+ page === undefined
53
+ ? presented(entry.authority, from)
54
+ : {
55
+ ...presented(entry.authority, { position: page.after, logEpoch: from.logEpoch }),
56
+ page: { through: page.through, read: page.read },
57
+ },
58
+ ],
35
59
  });
36
60
  const classified = classifyRead(answer.status, answer.body, entry.locator);
37
61
  if (classified !== null)
38
62
  return classified;
39
- return parseEnumeration(answer.body, 'site') ?? UNCERTAIN;
63
+ const found = parseEnumeration(answer.body, 'site');
64
+ if (found === null)
65
+ return UNCERTAIN;
66
+ return viewOf(found, entry) ?? { kind: 'room-not-readable', locator: entry.locator };
67
+ }
68
+ async function enumeration(entry, from) {
69
+ const first = await enumerated(entry, from);
70
+ return 'kind' in first ? first : wholeView(first, (page) => enumerated(entry, from, page));
40
71
  }
41
- async function caughtUp(entry) {
42
- const answer = await transport.read(`${SITE_DOOR_BOOTSTRAP}${entry.authority}`);
72
+ async function bootstrapPart(entry, after, offset) {
73
+ const query = [
74
+ ...(after === null ? [] : [`after=${encodeURIComponent(after)}`]),
75
+ ...(offset === null ? [] : [`offset=${offset}`]),
76
+ ];
77
+ const answer = await transport.read(`${SITE_DOOR_BOOTSTRAP}${entry.authority}${query.length === 0 ? '' : `?${query.join('&')}`}`);
43
78
  const classified = classifyDoor(answer.status, answer.body, entry.locator);
44
79
  if (classified !== null)
45
80
  return { failure: classified };
46
- const scope = parseSiteBootstrap(answer.body);
47
- if (scope === null)
48
- return { failure: UNCERTAIN };
49
- if (roomVersion(scope) === roomVersion(entry))
50
- return { scope };
51
- const found = await enumeration(entry, scope);
52
- if ('kind' in found)
53
- return { failure: settled(found) };
54
- const view = viewOf(found, entry);
55
- if (view === undefined)
56
- return { failure: { kind: 'room-not-readable', locator: entry.locator } };
57
- return { scope: advancedScope(scope, view) };
81
+ const scope = await stepped(parseSiteBootstrap(answer.body));
82
+ return scope === null ? { failure: UNCERTAIN } : { body: answer.body, scope };
83
+ }
84
+ async function assembled(entry, scope, path, offset) {
85
+ const opened = scope.contents.at(-1);
86
+ if (opened === undefined || opened.path !== path || opened.content.length !== offset) {
87
+ return UNCERTAIN;
88
+ }
89
+ let text = opened.content;
90
+ for (;;) {
91
+ const part = await bootstrapPart(entry, path, text.length);
92
+ if ('failure' in part)
93
+ return part.failure;
94
+ const [piece] = part.scope.contents;
95
+ if (piece === undefined ||
96
+ part.scope.contents.length !== 1 ||
97
+ piece.path !== path ||
98
+ piece.content.length === 0) {
99
+ return UNCERTAIN;
100
+ }
101
+ text += piece.content;
102
+ const more = siteBootstrapOffset(part.body);
103
+ if (more === null)
104
+ break;
105
+ if (more !== text.length)
106
+ return UNCERTAIN;
107
+ }
108
+ const file = scope.files.find((held) => held.path === path);
109
+ if (file === undefined || (await sha256Hex(text)) !== file.version)
110
+ return UNCERTAIN;
111
+ return { ...scope, contents: [...scope.contents.slice(0, -1), { path, content: text }] };
112
+ }
113
+ async function caughtUp(entry, after) {
114
+ const part = await bootstrapPart(entry, after, null);
115
+ if ('failure' in part)
116
+ return part;
117
+ const next = siteBootstrapNext(part.body);
118
+ const offset = siteBootstrapOffset(part.body);
119
+ if (offset !== null) {
120
+ if (next === null)
121
+ return { failure: UNCERTAIN };
122
+ const scope = await assembled(entry, part.scope, next, offset);
123
+ return 'kind' in scope ? { failure: scope } : { scope, next };
124
+ }
125
+ const { scope } = part;
126
+ if (after !== null || next !== null || roomVersion(scope) === roomVersion(entry)) {
127
+ return { scope, next };
128
+ }
129
+ const view = await enumeration(entry, scope);
130
+ if ('kind' in view)
131
+ return { failure: settled(view) };
132
+ return { scope: await stepped(advancedScope(scope, view)), next: null };
58
133
  }
59
134
  async function refreshed(entry, fromRoomVersion) {
60
135
  const from = logPosition(fromRoomVersion);
61
136
  if (from === null)
62
137
  return { kind: 'beyond-horizon' };
63
- const streamed = notifications.advance(entry, from);
138
+ const streamed = await stepped(notifications.advance(entry, from));
64
139
  if (streamed !== null) {
65
140
  const models = await streamedModels(streamed.schemas, entry, 'site');
66
141
  return models === undefined ? streamed.page : { ...streamed.page, models };
67
142
  }
68
- const found = await enumeration(entry, from);
69
- if ('kind' in found)
70
- return found;
71
- const view = viewOf(found, entry);
72
- if (view === undefined)
73
- return { kind: 'room-not-readable', locator: entry.locator };
143
+ const view = await enumeration(entry, from);
144
+ if ('kind' in view)
145
+ return view;
74
146
  notifications.reach(entry.authority, view);
75
- const answer = refreshFrom(view, fromRoomVersion);
147
+ const answer = await refreshFrom(view, fromRoomVersion);
76
148
  if (answer.kind !== 'net-advancement')
77
149
  return answer;
78
150
  const models = await viewModels(view, entry);
@@ -80,6 +152,7 @@ export function createSiteAdapter(transport) {
80
152
  }
81
153
  return {
82
154
  localSaves: true,
155
+ exhaustiveDiscovery: true,
83
156
  async authorize() {
84
157
  return {};
85
158
  },
@@ -102,14 +175,24 @@ export function createSiteAdapter(transport) {
102
175
  const entry = tracked.get(request.locator);
103
176
  if (entry === undefined)
104
177
  return { kind: 'room-not-readable', locator: request.locator };
105
- if (request.pageToken !== undefined)
106
- return UNCERTAIN;
107
- const held = await caughtUp(entry);
178
+ const continued = request.pageToken === undefined
179
+ ? null
180
+ : JSON.parse(request.pageToken);
181
+ const held = await caughtUp(entry, continued?.after ?? null);
108
182
  if ('failure' in held)
109
183
  return held.failure;
110
184
  notifications.reach(entry.authority, held.scope);
111
- const models = await scopeModels(held.scope, entry);
112
- return { ...bootstrapOf(held.scope), ...(models === undefined ? {} : { models }) };
185
+ const seen = [
186
+ ...(continued?.seen ?? []),
187
+ ...entry.schemas.filter((schema) => held.scope.files.some((file) => file.path === schema)),
188
+ ];
189
+ const models = await partModels(held.scope, entry, new Set(seen), held.next === null);
190
+ const continuation = held.next === null ? null : { after: held.next, seen };
191
+ return {
192
+ ...(await stepped(bootstrapping(held.scope))),
193
+ ...(models === undefined ? {} : { models }),
194
+ ...(continuation === null ? {} : { pageToken: JSON.stringify(continuation) }),
195
+ };
113
196
  },
114
197
  async refresh(_authorization, request) {
115
198
  const entry = tracked.get(request.locator);
@@ -117,6 +200,11 @@ export function createSiteAdapter(transport) {
117
200
  return { kind: 'room-not-readable', locator: request.locator };
118
201
  if (request.pageToken !== undefined)
119
202
  return UNCERTAIN;
203
+ if (snapshot) {
204
+ return request.fromRoomVersion === roomVersion(entry)
205
+ ? { kind: 'still-current' }
206
+ : { kind: 'beyond-horizon' };
207
+ }
120
208
  return refreshed(entry, request.fromRoomVersion);
121
209
  },
122
210
  async mutate() {
@@ -126,24 +214,26 @@ export function createSiteAdapter(transport) {
126
214
  return { kind: 'no-history' };
127
215
  },
128
216
  async binary(_authorization, request) {
129
- const named = published(request.kind === 'path' ? { room: request.locator, path: request.path } : request.referrer);
130
- if (named === null) {
131
- return { kind: 'rejected', code: 'referrer_unknown', fields: {}, retryable: false };
132
- }
133
- if (request.fileRef === undefined) {
134
- return { kind: 'rejected', code: 'file_not_found', fields: {}, retryable: false };
135
- }
136
- const { room_ref, path, record } = named.referrer;
137
- const read = await transport.download(`${SITE_FILES}${room_ref}/${encodeURIComponent(request.fileRef)}/${path === undefined
138
- ? `${SITE_FILE_RECORD}/${encodeURIComponent(record)}`
139
- : encodeURIComponent(path)}`);
140
- if (read.status === 404) {
141
- return { kind: 'rejected', code: 'file_not_found', fields: {}, retryable: false };
142
- }
217
+ const route = fileRoute(request);
218
+ if (typeof route !== 'string')
219
+ return route;
220
+ const read = await transport.download(route);
221
+ if (read.status === 404)
222
+ return unfound();
143
223
  return read.status === 200 && read.blob !== null
144
224
  ? { kind: 'binary', blob: read.blob }
145
225
  : UNCERTAIN;
146
226
  },
227
+ async link(_authorization, request) {
228
+ const route = fileRoute(request);
229
+ if (typeof route !== 'string')
230
+ return route;
231
+ return {
232
+ kind: 'link',
233
+ url: route,
234
+ name: request.path.slice(request.path.lastIndexOf('/') + 1),
235
+ };
236
+ },
147
237
  referrerScope(referrer) {
148
238
  const named = published(referrer);
149
239
  if (named === null)
@@ -170,20 +260,27 @@ export function createSiteAdapter(transport) {
170
260
  return UNCERTAIN;
171
261
  return actionOutcome(await transport.door(SITE_DOOR_SUBMIT, request.input), SITE_SUBMIT);
172
262
  },
173
- roomStream: {
174
- hold(rooms) {
175
- notifications.send(rooms.flatMap((room) => {
176
- const entry = tracked.get(room.locator);
177
- const at = logPosition(room.roomVersion);
178
- if (entry === undefined || at === null)
179
- return [];
180
- notifications.reach(entry.authority, at);
181
- return [presented(entry.authority, at)];
182
- }));
263
+ roomStream: snapshot
264
+ ? undefined
265
+ : {
266
+ hold(rooms) {
267
+ notifications.send(rooms.flatMap((room) => {
268
+ const entry = tracked.get(room.locator);
269
+ const at = logPosition(room.roomVersion);
270
+ if (entry === undefined || at === null)
271
+ return [];
272
+ notifications.reach(entry.authority, at);
273
+ return [presented(entry.authority, at)];
274
+ }));
275
+ },
276
+ reopen: notifications.reopen,
277
+ observe: notifications.observe,
278
+ published(locator, roomVersion) {
279
+ const at = logPosition(roomVersion);
280
+ if (at !== null)
281
+ notifications.published(locator, at);
282
+ },
183
283
  },
184
- reopen: notifications.reopen,
185
- observe: notifications.observe,
186
- },
187
284
  serviceMode: notifications.serviceMode,
188
285
  };
189
286
  }
package/dist/stream.d.ts CHANGED
@@ -1,21 +1,21 @@
1
- import type { ConversationStream, RefreshPage, RoomStreamEvent, ServiceModeSignal } from '@agentshouse/mdruntime';
1
+ import { type ConversationStream, type RefreshPage, type RoomStreamEvent, type ServiceModeSignal, type StreamReceipts } from '@agentshouse/mdruntime';
2
2
  import type { TrackedAuthority } from './discovery.js';
3
3
  import type { HouseDoor, HouseHeldRoom, HouseMode, HouseStreamEntry } from './transport.js';
4
4
  import { type LogPosition } from './version.js';
5
5
  export type HeldRevision = (path: string, revision: string) => boolean;
6
6
  export type StreamedAdvancement = {
7
7
  page: RefreshPage;
8
- entries: readonly HouseStreamEntry[];
9
8
  schemas: ReadonlyMap<string, HouseStreamEntry>;
10
9
  };
11
10
  export type HouseNotifications = {
12
11
  runtime: string;
13
- observe(notify: (event: RoomStreamEvent) => void): () => void;
12
+ observe(notify: (event: RoomStreamEvent) => void, receipts: StreamReceipts): () => void;
14
13
  send(rooms: readonly HouseHeldRoom[]): void;
14
+ published(locator: string, at: LogPosition): void;
15
15
  reopen(): void;
16
16
  track(authorities: readonly TrackedAuthority[]): void;
17
17
  reach(authority: string, at: LogPosition): void;
18
- advance(tracked: TrackedAuthority, from: LogPosition, holds?: HeldRevision): StreamedAdvancement | null;
18
+ advance(tracked: TrackedAuthority, from: LogPosition, holds?: HeldRevision): Generator<number, StreamedAdvancement | null, void>;
19
19
  conversations: ConversationStream;
20
20
  serviceMode: ServiceModeSignal;
21
21
  };
@@ -1 +1 @@
1
- {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,kBAAkB,EAElB,WAAW,EACX,eAAe,EAEf,iBAAiB,EAElB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EAEb,SAAS,EAET,gBAAgB,EAEjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAgB7D,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;AAEvE,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACrC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAC9D,IAAI,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI,CAAC;IAC5C,MAAM,IAAI,IAAI,CAAC;IACf,KAAK,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,GAAG,IAAI,CAAC;IACtD,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,GAAG,IAAI,CAAC;IAChD,OAAO,CACL,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE,WAAW,EACjB,KAAK,CAAC,EAAE,YAAY,GACnB,mBAAmB,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,kBAAkB,CAAC;IAClC,WAAW,EAAE,iBAAiB,CAAC;CAChC,CAAC;AAuBF,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,GAAG,kBAAkB,CA8S5F"}
1
+ {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,kBAAkB,EAEvB,KAAK,WAAW,EAChB,KAAK,eAAe,EAEpB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EAEpB,MAAM,wBAAwB,CAAC;AAGhC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EAEb,SAAS,EAET,gBAAgB,EAEjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AA0C7D,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;AAEvE,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,EAAE,QAAQ,EAAE,cAAc,GAAG,MAAM,IAAI,CAAC;IACxF,IAAI,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI,CAAC;IAC5C,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,GAAG,IAAI,CAAC;IAClD,MAAM,IAAI,IAAI,CAAC;IACf,KAAK,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,GAAG,IAAI,CAAC;IACtD,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,GAAG,IAAI,CAAC;IAChD,OAAO,CACL,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE,WAAW,EACjB,KAAK,CAAC,EAAE,YAAY,GACnB,SAAS,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI,EAAE,IAAI,CAAC,CAAC;IACvD,aAAa,EAAE,kBAAkB,CAAC;IAClC,WAAW,EAAE,iBAAiB,CAAC;CAChC,CAAC;AAmFF,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,GAAG,kBAAkB,CA0iB5F"}