@ai-matrx/agents 0.18.1 → 0.19.1

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.
@@ -204,6 +204,14 @@ interface SourceDraft {
204
204
  notes?: string[] | undefined;
205
205
  /** The person asked to wait for the clean version before anything runs. */
206
206
  waitForClean?: boolean | undefined;
207
+ /**
208
+ * Where the picked parts sit in the Source (label + pages), remembered when
209
+ * they are picked. A Source still being cleaned lists its raw pages; when the
210
+ * clean version lands its parts have NEW ids, and the pick is carried over by
211
+ * page from here (verify-3: "0 characters · 4 of 21 parts"). Persisted with
212
+ * the draft, so it survives a reload.
213
+ */
214
+ pickedParts?: SourcePickedPart[] | undefined;
207
215
  /**
208
216
  * What the person handed over, kept ONLY while the Source is still landing
209
217
  * (or failed) so a reload or a failure never loses it: the pasted text, the
@@ -212,6 +220,13 @@ interface SourceDraft {
212
220
  */
213
221
  input?: SourceIntakeInput | undefined;
214
222
  }
223
+ /** One picked part as the person saw it: its id, its label, and the pages it covers. */
224
+ interface SourcePickedPart {
225
+ id: string;
226
+ label: string;
227
+ /** First and last page it covers, when the Source has pages. */
228
+ pages?: [number, number] | undefined;
229
+ }
215
230
  type SourceCardStatus = "pending" | "resolving" | "ready" | "error";
216
231
  /** One card: the draft, its lifecycle, and what the server measured. */
217
232
  interface SourceCardModel {
@@ -432,8 +447,14 @@ interface SourceSetController extends SourceSetActions {
432
447
  toSourceSet(options?: {
433
448
  targetModelId?: string | undefined;
434
449
  }): SourceSet;
435
- /** Apply a set the review returned (forms, parts, caps, delivery). */
436
- applySourceSet(set: SourceSet): void;
450
+ /**
451
+ * Apply a set the review returned (forms, parts, caps, delivery). Pass
452
+ * `reviewed` — the set sent INTO the review — and every Source removed there
453
+ * leaves the list too (only those; one that landed meanwhile stays).
454
+ */
455
+ applySourceSet(set: SourceSet, options?: {
456
+ reviewed?: SourceSet | undefined;
457
+ }): void;
437
458
  /** POST /sources/resolve — the grounded text for a generator, named as the cards name them. */
438
459
  resolve(options?: {
439
460
  targetModelId?: string | undefined;
@@ -591,15 +612,33 @@ interface SourceIntakeDeps<Body extends SourceLandingBodyBase = SourceLandingBod
591
612
  interface SourceIntake {
592
613
  addPastedText(text: string, name?: string): Promise<void>;
593
614
  addWebPage(url: string): Promise<void>;
594
- /** A page the host's web picker already read (and the scraper landed); null id = land its text instead. */
615
+ /**
616
+ * A page the host's web picker already read (and the scraper landed); null id
617
+ * = land its text instead. `pendingId` = the card `beginWebPage` made when the
618
+ * read started — it is settled instead of a new card being added.
619
+ */
595
620
  addScrapedPage(page: {
596
621
  url: string;
597
622
  title: string;
598
623
  text: string;
599
624
  processedDocumentId: string | null;
600
- }): Promise<void>;
625
+ }, pendingId?: string | null): Promise<void>;
626
+ /**
627
+ * The host's web picker STARTED reading a link: a card with the link kept
628
+ * appears at once, so a reload mid-read brings it back and lands it (verify-3
629
+ * 12a/12b: a reload while a page was read lost it, link and all). Finish it
630
+ * with `addScrapedPage(page, id)`; if the person drops the preview, remove it;
631
+ * if the picker goes away mid-read, `resume(card)` lands it through the web door.
632
+ */
633
+ beginWebPage(url: string): string;
601
634
  /** Files the host's uploader already uploaded. */
602
635
  addUploaded(files: readonly UploadedSourceFile[], kind: SourceKindId): Promise<void>;
636
+ /**
637
+ * A recording made in place (the Voice Pad) that is already written out: it
638
+ * lands as text, but it IS a recording — its card and review say "Recording",
639
+ * never "Pasted text" — and it keeps its name ("Recording 3:41 PM").
640
+ */
641
+ addRecordedText(text: string, name?: string): Promise<void>;
603
642
  /** A recording already uploaded — written out, then landed. */
604
643
  addUploadedRecording(file: UploadedSourceFile): Promise<void>;
605
644
  /** Something the person already has (Use existing / search). Returns false when already picked. */
@@ -690,7 +729,9 @@ interface UseSourceSetResult extends SourceSetActions {
690
729
  targetModelId?: string | undefined;
691
730
  }) => SourceSet;
692
731
  /** Apply a set the review page returned (forms, parts, caps, delivery). */
693
- applySourceSet: (set: SourceSet) => void;
732
+ applySourceSet: (set: SourceSet, options?: {
733
+ reviewed?: SourceSet | undefined;
734
+ }) => void;
694
735
  /** POST /sources/resolve — the grounded text for a generator. */
695
736
  resolve: (options?: {
696
737
  targetModelId?: string | undefined;
@@ -204,6 +204,14 @@ interface SourceDraft {
204
204
  notes?: string[] | undefined;
205
205
  /** The person asked to wait for the clean version before anything runs. */
206
206
  waitForClean?: boolean | undefined;
207
+ /**
208
+ * Where the picked parts sit in the Source (label + pages), remembered when
209
+ * they are picked. A Source still being cleaned lists its raw pages; when the
210
+ * clean version lands its parts have NEW ids, and the pick is carried over by
211
+ * page from here (verify-3: "0 characters · 4 of 21 parts"). Persisted with
212
+ * the draft, so it survives a reload.
213
+ */
214
+ pickedParts?: SourcePickedPart[] | undefined;
207
215
  /**
208
216
  * What the person handed over, kept ONLY while the Source is still landing
209
217
  * (or failed) so a reload or a failure never loses it: the pasted text, the
@@ -212,6 +220,13 @@ interface SourceDraft {
212
220
  */
213
221
  input?: SourceIntakeInput | undefined;
214
222
  }
223
+ /** One picked part as the person saw it: its id, its label, and the pages it covers. */
224
+ interface SourcePickedPart {
225
+ id: string;
226
+ label: string;
227
+ /** First and last page it covers, when the Source has pages. */
228
+ pages?: [number, number] | undefined;
229
+ }
215
230
  type SourceCardStatus = "pending" | "resolving" | "ready" | "error";
216
231
  /** One card: the draft, its lifecycle, and what the server measured. */
217
232
  interface SourceCardModel {
@@ -432,8 +447,14 @@ interface SourceSetController extends SourceSetActions {
432
447
  toSourceSet(options?: {
433
448
  targetModelId?: string | undefined;
434
449
  }): SourceSet;
435
- /** Apply a set the review returned (forms, parts, caps, delivery). */
436
- applySourceSet(set: SourceSet): void;
450
+ /**
451
+ * Apply a set the review returned (forms, parts, caps, delivery). Pass
452
+ * `reviewed` — the set sent INTO the review — and every Source removed there
453
+ * leaves the list too (only those; one that landed meanwhile stays).
454
+ */
455
+ applySourceSet(set: SourceSet, options?: {
456
+ reviewed?: SourceSet | undefined;
457
+ }): void;
437
458
  /** POST /sources/resolve — the grounded text for a generator, named as the cards name them. */
438
459
  resolve(options?: {
439
460
  targetModelId?: string | undefined;
@@ -591,15 +612,33 @@ interface SourceIntakeDeps<Body extends SourceLandingBodyBase = SourceLandingBod
591
612
  interface SourceIntake {
592
613
  addPastedText(text: string, name?: string): Promise<void>;
593
614
  addWebPage(url: string): Promise<void>;
594
- /** A page the host's web picker already read (and the scraper landed); null id = land its text instead. */
615
+ /**
616
+ * A page the host's web picker already read (and the scraper landed); null id
617
+ * = land its text instead. `pendingId` = the card `beginWebPage` made when the
618
+ * read started — it is settled instead of a new card being added.
619
+ */
595
620
  addScrapedPage(page: {
596
621
  url: string;
597
622
  title: string;
598
623
  text: string;
599
624
  processedDocumentId: string | null;
600
- }): Promise<void>;
625
+ }, pendingId?: string | null): Promise<void>;
626
+ /**
627
+ * The host's web picker STARTED reading a link: a card with the link kept
628
+ * appears at once, so a reload mid-read brings it back and lands it (verify-3
629
+ * 12a/12b: a reload while a page was read lost it, link and all). Finish it
630
+ * with `addScrapedPage(page, id)`; if the person drops the preview, remove it;
631
+ * if the picker goes away mid-read, `resume(card)` lands it through the web door.
632
+ */
633
+ beginWebPage(url: string): string;
601
634
  /** Files the host's uploader already uploaded. */
602
635
  addUploaded(files: readonly UploadedSourceFile[], kind: SourceKindId): Promise<void>;
636
+ /**
637
+ * A recording made in place (the Voice Pad) that is already written out: it
638
+ * lands as text, but it IS a recording — its card and review say "Recording",
639
+ * never "Pasted text" — and it keeps its name ("Recording 3:41 PM").
640
+ */
641
+ addRecordedText(text: string, name?: string): Promise<void>;
603
642
  /** A recording already uploaded — written out, then landed. */
604
643
  addUploadedRecording(file: UploadedSourceFile): Promise<void>;
605
644
  /** Something the person already has (Use existing / search). Returns false when already picked. */
@@ -690,7 +729,9 @@ interface UseSourceSetResult extends SourceSetActions {
690
729
  targetModelId?: string | undefined;
691
730
  }) => SourceSet;
692
731
  /** Apply a set the review page returned (forms, parts, caps, delivery). */
693
- applySourceSet: (set: SourceSet) => void;
732
+ applySourceSet: (set: SourceSet, options?: {
733
+ reviewed?: SourceSet | undefined;
734
+ }) => void;
694
735
  /** POST /sources/resolve — the grounded text for a generator. */
695
736
  resolve: (options?: {
696
737
  targetModelId?: string | undefined;
@@ -100,12 +100,12 @@ function deliveryPatch(delivery) {
100
100
  var DELIVERY_WORDS = {
101
101
  direct: {
102
102
  label: "Include the text",
103
- hint: "The text is handed to the AI with your request, so it reads all of it before it starts.",
103
+ hint: "Sent in full",
104
104
  summary: "Text included"
105
105
  },
106
106
  context: {
107
107
  label: "Let the AI look it up",
108
- hint: "Nothing is sent up front. The AI opens this Source and reads the parts it needs while it works \u2014 best for very large Sources.",
108
+ hint: "Read as needed",
109
109
  summary: "Looked up when needed"
110
110
  }
111
111
  };
@@ -128,6 +128,13 @@ function fitDelivery(ref, deliveries) {
128
128
 
129
129
  // sources/runtime/partsSearch.ts
130
130
  var RANGE_RE = /^(\d+)\s*(?:-|–|—|to)\s*(\d+)$/;
131
+ var SPAN_RE = /(\d+)\s*[–—-]\s*(\d+)/;
132
+ function partPages(part) {
133
+ if (part.page === void 0 || part.page === null) return null;
134
+ const span = /^pages?\s/i.test(part.label) ? SPAN_RE.exec(part.label) : null;
135
+ if (span) return [Number(span[1]), Number(span[2])];
136
+ return [part.page, part.page];
137
+ }
131
138
  function isWordQuery(query) {
132
139
  const q = query.trim().toLowerCase();
133
140
  return q.length > 0 && !/^\d+$/.test(q) && !RANGE_RE.test(q);
@@ -146,7 +153,7 @@ function resumableInput(draft) {
146
153
  case "youtube":
147
154
  return input.url?.trim() ? input : null;
148
155
  case "audio":
149
- return input.fileId ? input : null;
156
+ return input.fileId || input.text?.trim() ? input : null;
150
157
  default:
151
158
  return null;
152
159
  }
@@ -161,12 +168,12 @@ function reloadedCard(draft, status) {
161
168
  return { action: "reoffer", sentence: uploadCutOffSentence(draft) };
162
169
  return { action: "lost", sentence: RELOADED_WHILE_ADDING };
163
170
  }
164
- var NOT_KEPT_FOR_RELOAD = "Too large to keep on this device, so it will not come back if the page reloads before it is added.";
171
+ var NOT_KEPT_FOR_RELOAD = "Too large to restore if the page reloads before it is added.";
165
172
  var WAITING_FOR_ORGANIZATION = "Waiting for an organization \u2014 choose one and this continues by itself.";
166
173
  var KEEP_WAITING_FOR_ORGANIZATION = "Read. Waiting for an organization to keep it \u2014 choose one and it is kept by itself.";
167
174
 
168
175
  // sources/runtime/fileSource.ts
169
- var UNREADABLE = "The file is stored, but it could not be read into a Source, so nothing in it can be used yet. Remove it, or add a different copy (a PDF, Word file or plain text reads best).";
176
+ var UNREADABLE = "Can't read this file \u2014 add a PDF, Word or text copy.";
170
177
  function nextFileStep(status, ctx) {
171
178
  const pdId = status?.processed_document_id ?? null;
172
179
  if (pdId) return { do: "keep", processedDocumentId: pdId };
@@ -174,7 +181,7 @@ function nextFileStep(status, ctx) {
174
181
  if (status?.state === "failed")
175
182
  return {
176
183
  do: "unreadable",
177
- reason: status.error?.message ? `The file could not be read: ${status.error.message}. Remove it, or add it again.` : UNREADABLE
184
+ reason: status.error?.message ? `Can't read this file: ${status.error.message}. Add it again.` : UNREADABLE
178
185
  };
179
186
  if (!ctx.started) return { do: "start" };
180
187
  return { do: "unreadable", reason: UNREADABLE };
@@ -195,6 +202,8 @@ function fileCardHeldForOrganization(card, activeOrganizationId, fileOrganizatio
195
202
 
196
203
  // sources/runtime/state.ts
197
204
  var EMPTY_SOURCE_SET_STATE = Object.freeze({ cards: Object.freeze([]), topic: "" });
205
+ var PARTS_MATCHED_BY_PAGE = "The clean version is ready \u2014 your parts were matched to it by page.";
206
+ var PARTS_NOT_MATCHED = "The clean version is ready, but your parts could not be matched to it \u2014 choose parts again. Until then the whole Source goes in.";
198
207
  var SAME_SOURCE_AGAIN = "You added this again \u2014 it is the same Source, so it is listed once.";
199
208
  function isSourceDraft(value) {
200
209
  return !!value && typeof value === "object" && typeof value.kind === "string" && typeof value.label === "string";
@@ -261,7 +270,7 @@ function settleCard(state, id, patch, options = {}) {
261
270
  }));
262
271
  }
263
272
  function failCard(state, id, sentence) {
264
- return replaceCard(state, id, (c) => ({ ...c, status: "error", error: sentence }));
273
+ return replaceCard(state, id, (c) => ({ ...c, status: "error", error: sentence, manifest: null }));
265
274
  }
266
275
  function restartCard(state, id) {
267
276
  return replaceCard(state, id, (c) => ({ ...c, status: "resolving", error: null }));
@@ -277,7 +286,68 @@ function updateCardRef(state, id, options) {
277
286
  return replaceCard(state, id, (c) => {
278
287
  if (!c.draft.ref) return c;
279
288
  const ref = createSourceRef(c.draft.ref.resource_type, c.draft.ref.resource_id, { ...c.draft.ref, ...options });
280
- return { ...c, draft: { ...c.draft, ref } };
289
+ return rememberPickedParts({ ...c, draft: { ...c.draft, ref } });
290
+ });
291
+ }
292
+ function rememberPickedParts(card) {
293
+ const ids = card.draft.ref?.include_segments ?? [];
294
+ if (ids.length === 0) {
295
+ return card.draft.pickedParts === void 0 ? card : { ...card, draft: { ...card.draft, pickedParts: void 0 } };
296
+ }
297
+ const segments = card.manifest?.segments ?? [];
298
+ const known = new Map((card.draft.pickedParts ?? []).map((p) => [p.id, p]));
299
+ const picked = [];
300
+ for (const id of ids) {
301
+ const segment = segments.find((s) => s.id === id);
302
+ if (segment) {
303
+ const pages = partPages(segment);
304
+ picked.push({ id, label: segment.label, ...pages ? { pages } : {} });
305
+ } else {
306
+ const before = known.get(id);
307
+ if (before) picked.push(before);
308
+ }
309
+ }
310
+ return { ...card, draft: { ...card.draft, pickedParts: picked.length ? picked : void 0 } };
311
+ }
312
+ function withNote(notes, note, drop = []) {
313
+ const kept = (notes ?? []).filter((n) => n !== note && !drop.includes(n));
314
+ return [...kept, note];
315
+ }
316
+ function carryPickedParts(card, entry) {
317
+ const ref = card.draft.ref;
318
+ const picked = ref?.include_segments ?? [];
319
+ const segments = entry.segments ?? [];
320
+ if (!ref || picked.length === 0 || segments.length === 0) return card;
321
+ const ids = new Set(segments.map((s) => s.id));
322
+ const stale = picked.filter((id) => !ids.has(id));
323
+ if (stale.length === 0) return card;
324
+ const remembered = new Map((card.draft.pickedParts ?? []).map((p) => [p.id, p]));
325
+ const oldSegments = card.manifest?.segments ?? [];
326
+ const spans = [];
327
+ let unknown = 0;
328
+ for (const id of stale) {
329
+ const old = oldSegments.find((s) => s.id === id);
330
+ const pages = remembered.get(id)?.pages ?? (old ? partPages(old) : null);
331
+ if (pages) spans.push(pages);
332
+ else unknown += 1;
333
+ }
334
+ const keep = new Set(picked.filter((id) => ids.has(id)));
335
+ for (const segment of segments) {
336
+ const pages = partPages(segment);
337
+ if (pages && spans.some(([from, to]) => pages[0] <= to && pages[1] >= from)) keep.add(segment.id);
338
+ }
339
+ const nextIds = segments.filter((s) => keep.has(s.id)).map((s) => s.id);
340
+ const matched = unknown === 0 && nextIds.length > 0;
341
+ const nextRef = createSourceRef(ref.resource_type, ref.resource_id, {
342
+ ...ref,
343
+ include_segments: matched ? nextIds : void 0
344
+ });
345
+ const note = matched ? PARTS_MATCHED_BY_PAGE : PARTS_NOT_MATCHED;
346
+ const other = matched ? PARTS_NOT_MATCHED : PARTS_MATCHED_BY_PAGE;
347
+ return rememberPickedParts({
348
+ ...card,
349
+ manifest: entry,
350
+ draft: { ...card.draft, ref: nextRef, notes: withNote(card.draft.notes, note, [other]) }
281
351
  });
282
352
  }
283
353
  function setCardWaitForClean(state, id, wait) {
@@ -286,14 +356,24 @@ function setCardWaitForClean(state, id, wait) {
286
356
  function setSourceTopic(state, topic) {
287
357
  return state.topic === topic ? state : { ...state, topic };
288
358
  }
289
- function applyReviewedSourceSet(state, set) {
359
+ function removedInReview(reviewed, answer) {
360
+ const kept = new Set(answer.sources.map(sourceKey));
361
+ return new Set(reviewed.sources.map(sourceKey).filter((k) => !kept.has(k)));
362
+ }
363
+ function applyReviewedSourceSet(state, set, reviewed) {
290
364
  let next = state;
291
- for (const card of state.cards) {
365
+ if (reviewed) {
366
+ const removed = removedInReview(reviewed, set);
367
+ for (const card of state.cards) {
368
+ if (card.draft.ref && removed.has(sourceKey(card.draft.ref))) next = removeCard(next, card.id);
369
+ }
370
+ }
371
+ for (const card of next.cards) {
292
372
  const ref = card.draft.ref;
293
373
  if (!ref) continue;
294
374
  const match = set.sources.find((r) => sameSourceRef(r, ref));
295
375
  if (!match) continue;
296
- next = replaceCard(next, card.id, (c) => ({ ...c, draft: { ...c.draft, ref: match } }));
376
+ next = replaceCard(next, card.id, (c) => rememberPickedParts({ ...c, draft: { ...c.draft, ref: match } }));
297
377
  }
298
378
  return set.topic !== void 0 ? setSourceTopic(next, set.topic) : next;
299
379
  }
@@ -303,7 +383,13 @@ function applyManifest(state, manifest) {
303
383
  const ref = card.draft.ref;
304
384
  if (!ref || card.status !== "ready") continue;
305
385
  const entry = manifest.sources.find((e) => sameSourceRef(e.ref, ref));
306
- if (entry) next = replaceCard(next, card.id, (c) => ({ ...c, manifest: entry }));
386
+ if (!entry) continue;
387
+ next = replaceCard(next, card.id, (c) => {
388
+ const carried = carryPickedParts(c, entry);
389
+ if (carried !== c) return carried;
390
+ const measured = { ...c, manifest: entry };
391
+ return c.draft.ref?.include_segments?.length && !c.draft.pickedParts ? rememberPickedParts(measured) : measured;
392
+ });
307
393
  }
308
394
  return next;
309
395
  }
@@ -498,8 +584,8 @@ function createSourceSetController(adapter) {
498
584
  hasRef: (resourceType, resourceId) => hasSourceRef(store.getState(), resourceType, resourceId),
499
585
  liveCount: () => store.getState().cards.length,
500
586
  toSourceSet: (options = {}) => selectSourceSet(store.getState(), options),
501
- applySourceSet(set) {
502
- commit(applyReviewedSourceSet(store.getState(), set));
587
+ applySourceSet(set, options = {}) {
588
+ commit(applyReviewedSourceSet(store.getState(), set, options.reviewed));
503
589
  },
504
590
  async manifest() {
505
591
  const refs = readySourceRefs(store.getState());
@@ -784,20 +870,45 @@ function createSourceIntake(set, deps) {
784
870
  });
785
871
  await hold(() => landPaste(id, trimmed, name));
786
872
  },
873
+ async addRecordedText(text, name) {
874
+ const trimmed = text.trim();
875
+ if (!trimmed) return;
876
+ const max = deps.maxKeptChars();
877
+ const kept = max !== null && trimmed.length <= max;
878
+ const id = set.addPending({
879
+ kind: "audio",
880
+ label: name?.trim() || "Recording",
881
+ origin: "Recording",
882
+ input: kept ? { text: trimmed, name } : void 0,
883
+ notes: kept ? void 0 : [NOT_KEPT_FOR_RELOAD]
884
+ });
885
+ await hold(() => landPaste(id, trimmed, name));
886
+ },
787
887
  async addWebPage(raw) {
788
888
  const url = /^https?:\/\//i.test(raw.trim()) ? raw.trim() : `https://${raw.trim()}`;
789
889
  const id = set.addPending({ kind: "web", label: hostOf(url), origin: url, input: { url } });
790
890
  await hold(() => landWebPage(id, url));
791
891
  },
792
- async addScrapedPage(page) {
892
+ beginWebPage(raw) {
893
+ const url = /^https?:\/\//i.test(raw.trim()) ? raw.trim() : `https://${raw.trim()}`;
894
+ return set.addPending({ kind: "web", label: hostOf(url), origin: url, input: { url } });
895
+ },
896
+ async addScrapedPage(page, pendingId) {
793
897
  const label = page.title || hostOf(page.url);
898
+ const cardFor = () => {
899
+ if (pendingId && set.restart(pendingId)) {
900
+ set.updateDraft(pendingId, { label, origin: page.url, input: { url: page.url } });
901
+ return pendingId;
902
+ }
903
+ return set.addPending({ kind: "web", label, origin: page.url, input: { url: page.url } });
904
+ };
794
905
  if (!page.processedDocumentId) {
795
- const id2 = set.addPending({ kind: "web", label, origin: page.url, input: { url: page.url } });
906
+ const id2 = cardFor();
796
907
  await hold(() => landPaste(id2, page.text, label));
797
908
  return;
798
909
  }
799
910
  const pdId = page.processedDocumentId;
800
- const id = set.addPending({ kind: "web", label, origin: page.url, input: { url: page.url } });
911
+ const id = cardFor();
801
912
  await hold(async () => {
802
913
  let notes = [];
803
914
  try {
@@ -872,7 +983,8 @@ function createSourceIntake(set, deps) {
872
983
  return true;
873
984
  }
874
985
  case "audio":
875
- void transcribeRecording(card.id, input.fileId ?? "", card.draft.label);
986
+ if (input.fileId) void transcribeRecording(card.id, input.fileId, card.draft.label);
987
+ else void landPaste(card.id, (input.text ?? "").trim(), input.name);
876
988
  return true;
877
989
  default:
878
990
  return false;