@foir/sdk 0.28.0 → 0.30.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.
@@ -678,3 +678,185 @@ export interface FoirResolveContext {
678
678
  longitude: number;
679
679
  } | null;
680
680
  }
681
+ /**
682
+ * Where a run, or one step of it, stands. HELD waits for a decision on an act
683
+ * it holds, or for an operation to answer; SKIPPED was refused or denied and
684
+ * never ran.
685
+ */
686
+ export type FoirAgentRunStatus = 'SCHEDULED' | 'STARTED' | 'HELD' | 'COMPLETED' | 'FAILED' | 'SKIPPED' | 'CANCELLED';
687
+ /** An act a run holds until someone decides it. */
688
+ export interface FoirAgentApproval {
689
+ /** What the act would do, in words. */
690
+ summary: string;
691
+ /** CALLER: the person who started the run decides it. TEAM: the project's team does. */
692
+ approver: 'CALLER' | 'TEAM';
693
+ decision: 'PENDING' | 'APPROVED' | 'DENIED' | 'EXPIRED';
694
+ reason?: string | null;
695
+ }
696
+ /** One step of a run: a model turn, a tool call, a check, or a child agent. */
697
+ export interface FoirAgentRunNode {
698
+ id: string;
699
+ parentId?: string | null;
700
+ /**
701
+ * PAGE_TOOL is a call to a tool the caller's page offered with the message,
702
+ * HELD until the page answers it with `answerPageTool`; DATA is data sent to
703
+ * the app's components, delivered as a DATA event.
704
+ */
705
+ kind: 'AGENT' | 'OPERATION' | 'VERB' | 'MCP_TOOL' | 'MODEL_TURN' | 'CHECK' | 'PAGE_TOOL' | 'DATA';
706
+ /** The tool, operation or agent the step is, by its key. */
707
+ key: string;
708
+ status: FoirAgentRunStatus;
709
+ error?: string | null;
710
+ input?: unknown;
711
+ result?: unknown;
712
+ approval?: FoirAgentApproval | null;
713
+ /** ISO-8601. */
714
+ startedAt?: string | null;
715
+ /** ISO-8601. */
716
+ completedAt?: string | null;
717
+ }
718
+ /**
719
+ * A run of an agent: its answer, once given, and every step it took. A refusal
720
+ * is an answer too: a message the agent will not take ends SKIPPED with the
721
+ * reason as its answer.
722
+ */
723
+ export interface FoirAgentRun {
724
+ id: string;
725
+ /** The agent's key. */
726
+ agent: string;
727
+ /** The thread the run answers in. Pass it as `threadId` to go on talking. */
728
+ threadId?: string | null;
729
+ status: FoirAgentRunStatus;
730
+ answer?: string | null;
731
+ error?: string | null;
732
+ /** What the run has spent so far, in credits. */
733
+ spentCredits: number;
734
+ nodes: FoirAgentRunNode[];
735
+ /** An agent with an output schema: its typed answer, once the run has finished. */
736
+ output?: unknown;
737
+ }
738
+ /** One message of a conversation with an agent. */
739
+ export interface FoirAgentThreadMessage {
740
+ id: string;
741
+ seq: number;
742
+ role: 'USER' | 'ASSISTANT';
743
+ /** Text, tool calls and their answers, as the run kept them. */
744
+ parts: unknown[];
745
+ runId?: string | null;
746
+ /** ISO-8601. */
747
+ createdAt?: string | null;
748
+ }
749
+ /** A conversation with an agent, kept for the agent's retention and then deleted. */
750
+ export interface FoirAgentThread {
751
+ id: string;
752
+ agent: string;
753
+ title: string;
754
+ messageCount: number;
755
+ createdAt?: string | null;
756
+ updatedAt?: string | null;
757
+ expiresAt?: string | null;
758
+ /** Present on `agentThread`; absent in a list of threads. */
759
+ messages?: FoirAgentThreadMessage[] | null;
760
+ /** A run still answering in the thread: follow it with `foir.ns.agents.onRun`. */
761
+ runningRunId?: string | null;
762
+ }
763
+ /**
764
+ * `startAgentRun` arguments. Requires `agents:execute` for the agent, held by
765
+ * the signed-in user's role or given by the key to whoever comes through it.
766
+ */
767
+ export interface FoirStartAgentRunArgs {
768
+ /** The agent's key. */
769
+ agent: string;
770
+ message: string;
771
+ /** Continue this conversation. Omitted starts a new one. */
772
+ threadId?: string;
773
+ /** Sent again after a lost answer, the same key answers with the same run. */
774
+ idempotencyKey?: string;
775
+ /**
776
+ * A visitor with nobody signed in: the secret their browser keeps, which
777
+ * makes the run and its thread theirs. Make it once per browser and keep it
778
+ * (see `visitorToken` in `@foir/sdk/agents`).
779
+ */
780
+ visitorToken?: string;
781
+ /**
782
+ * Files sent with the message, each uploaded first with
783
+ * `createAgentAttachment` (or `uploadAgentFile`). At most 5; the message
784
+ * may then be empty.
785
+ */
786
+ attachmentIds?: string[];
787
+ /**
788
+ * What the message points at, by id: the agent reads each with its own
789
+ * tools, as the caller, so a tag reaches nothing the caller could not. At
790
+ * most 20.
791
+ */
792
+ tags?: FoirMessageTag[];
793
+ /**
794
+ * Tools of the caller's own page the agent may call during this run, for
795
+ * an agent that allows the page's tools. A call is HELD until the page
796
+ * answers it with `answerPageTool`. At most 20. `foirChatTransport`'s
797
+ * `pageTools` sends these and answers the calls for you.
798
+ */
799
+ pageTools?: FoirPageToolOffer[];
800
+ }
801
+ /** A tool of the caller's page, offered to an agent for one run. */
802
+ export interface FoirPageToolOffer {
803
+ /** Lowercase letters, digits and _, starting with a letter; not the name of one of the agent's own tools. */
804
+ name: string;
805
+ /** What the tool does, as the model is told it. At most 1000 characters. */
806
+ description?: string;
807
+ /** The JSON Schema of the tool's input: an object schema. Left out, the tool takes no input. */
808
+ inputSchema?: Record<string, unknown>;
809
+ }
810
+ /** What a tag in a message points at. */
811
+ export type FoirMessageTagKind = 'RECORD' | 'MODEL' | 'PERSON' | 'FILE' | 'OPERATION' | 'AGENT' | 'HOOK' | 'SCHEDULE' | 'ORGANIZATION' | 'NOTE' | 'FIELD';
812
+ /** One thing a message points at. It is kept by id and never resolved into the message. */
813
+ export interface FoirMessageTag {
814
+ kind: FoirMessageTagKind;
815
+ /** The thing's id, or its key where it is addressed by one (a model, an operation, an agent, a schedule). */
816
+ id: string;
817
+ /** A record's or a field's model, by key. */
818
+ model?: string;
819
+ /** A field's path within its record. */
820
+ field?: string;
821
+ /** What the person saw when they picked it. */
822
+ label?: string;
823
+ }
824
+ /** A file sent an agent with a message: an image or a PDF. */
825
+ export interface FoirAgentAttachment {
826
+ id: string;
827
+ filename: string;
828
+ mediaType: string;
829
+ /** Its length in bytes. */
830
+ size: number;
831
+ }
832
+ /**
833
+ * `createAgentAttachment` arguments: a PNG, JPEG, GIF or WebP image of at most
834
+ * 5 MB, or a PDF of at most 10 MB. A visitor may send images only, 20 a day.
835
+ */
836
+ export interface FoirCreateAgentAttachmentArgs {
837
+ agent: string;
838
+ filename: string;
839
+ mediaType: string;
840
+ /** The file's exact length in bytes. */
841
+ size: number;
842
+ visitorToken?: string;
843
+ }
844
+ /** Where a file's bytes go before it is sent. */
845
+ export interface FoirAgentAttachmentUpload {
846
+ attachment: FoirAgentAttachment;
847
+ /** An HTTP PUT of exactly `size` bytes, with `mediaType` as its Content-Type. */
848
+ uploadUrl: string;
849
+ /** When `uploadUrl` stops taking bytes; an attachment not sent by then is removed. */
850
+ expiresAt: string;
851
+ }
852
+ /** `agentThreads` — your conversations with the project's agents, newest first. */
853
+ export type FoirAgentThreadsResult = FoirConnection<FoirAgentThread>;
854
+ /** Your own account at a remote server a project's agent calls for you. */
855
+ export interface FoirMcpConnection {
856
+ id: string;
857
+ agent: string;
858
+ server: string;
859
+ scopes: string[];
860
+ connectedAt?: string | null;
861
+ expiresAt?: string | null;
862
+ }
@@ -58,8 +58,8 @@ export type { FoirManifest, FoirManifestComposite, FoirManifestModel, FoirManife
58
58
  export { planFoirModelCall, planFoirNamespaceCall } from './document';
59
59
  export type { FoirCall, FoirDocumentPlan, FoirNamespaceCall, FoirNamespaceSelections, FoirRecordPath, FoirSelectionModel, FoirSelectionNamespace, FoirSelections, FoirSelectionSource, FoirSelectionUnions, } from './document';
60
60
  export { createFoirSubjectRegistry, recordFoirSubjects } from './subjects';
61
- export type { FoirSubject, FoirSubjectEntry, FoirSubjectOptions, FoirSubjectOrigin, FoirSubjectRegistry, } from './subjects';
62
- export type { CollabInfo, FileValue, FoirAccessGrant, FoirAccessLevel, FoirAccessSource, FoirAccessor, FoirCreateMyNoteArgs, FoirWriteNoteArgs, FoirUserNote, FoirUserNoteAttachment, FoirDegraded, FoirDepthBelow, FoirDepthLevel, FoirDeviceType, FoirGranteeKind, FoirJsonValue, FoirMyAccess, FoirMyNotesArgs, FoirMyNotesResult, FoirNoteConfig, FoirNoteReceipt, FoirNoteSubjectType, FoirPlatformType, FoirRecordOwner, FoirResolveContext, FoirResourceAccess, FoirUnexpandedRecord, FoirUpdateManyPayload, FoirVisibility, GeoPoint, Image, ImageFormat, ImageVariant, ImageVariantSet, MediaValue, Passage, Prose, ProseSpan, RecordAncestor, Reference, Richtext, Video, } from './builtins';
61
+ export type { FoirFieldMark, FoirFieldMarkAttributes, FoirSubject, FoirSubjectEntry, FoirSubjectOptions, FoirSubjectOrigin, FoirSubjectRegistry, } from './subjects';
62
+ export type { CollabInfo, FileValue, FoirAccessGrant, FoirAccessLevel, FoirAccessSource, FoirAccessor, FoirCreateMyNoteArgs, FoirWriteNoteArgs, FoirUserNote, FoirUserNoteAttachment, FoirDegraded, FoirDepthBelow, FoirDepthLevel, FoirDeviceType, FoirGranteeKind, FoirJsonValue, FoirMyAccess, FoirMyNotesArgs, FoirMyNotesResult, FoirAgentApproval, FoirAgentAttachment, FoirAgentAttachmentUpload, FoirAgentRun, FoirAgentRunNode, FoirAgentRunStatus, FoirAgentThread, FoirAgentThreadMessage, FoirAgentThreadsResult, FoirMcpConnection, FoirStartAgentRunArgs, FoirPageToolOffer, FoirCreateAgentAttachmentArgs, FoirMessageTag, FoirMessageTagKind, FoirNoteConfig, FoirNoteReceipt, FoirNoteSubjectType, FoirPlatformType, FoirRecordOwner, FoirResolveContext, FoirResourceAccess, FoirUnexpandedRecord, FoirUpdateManyPayload, FoirVisibility, GeoPoint, Image, ImageFormat, ImageVariant, ImageVariantSet, MediaValue, Passage, Prose, ProseSpan, RecordAncestor, Reference, Richtext, Video, } from './builtins';
63
63
  /**
64
64
  * Pre-rename spellings. The `<Name>Block` wrappers and the `<Name>Value`
65
65
  * split are the same thing as the types above — an item IS its value — so each
@@ -76,10 +76,12 @@ export { foirSocketEndpoint, FoirSocketTransport } from './ws-transport';
76
76
  export type { FoirReconnectPolicy, FoirSubscribeRequest, FoirSubscription, FoirSubscriptionHandlers, FoirWebSocketFactory, FoirWebSocketLike, } from './ws-transport';
77
77
  export { printSubscriptionDocument } from './subscription-document';
78
78
  export type { FoirSubscriptionArgument, PrintSubscriptionDocumentInput, } from './subscription-document';
79
- export type { FoirChangeSubscribeOptions, FoirExecutionSubscribeOptions, FoirJobProgressEvent, FoirModelSubscriptions, FoirNamespaceSubscriptions, FoirNotificationEvent, FoirOperationExecutionEvent, FoirRecordChangedEvent, } from './subscriptions';
79
+ export type { FoirChangeSubscribeOptions, FoirExecutionSubscribeOptions, FoirAgentRunEvent, FoirAgentRunSubscribeOptions, FoirJobProgressEvent, FoirModelSubscriptions, FoirNamespaceSubscriptions, FoirNotificationEvent, FoirOperationExecutionEvent, FoirRecordChangedEvent, } from './subscriptions';
80
80
  export type { FoirConnection, FoirFieldsShape, FoirModelDefinition, FoirNamespaceDefinition, FoirNamespaceOpDefinition, FoirOpDefinition, FoirRelationDefinition, FoirReturnDefinition, FoirSchemaDefinition, } from './schema';
81
81
  export type { FoirInclude, FoirModelOperation, FoirNamespaceOperation, FoirOmittableKey, FoirOpResult, FoirQuery, FoirRecordResult, FoirWriteResult, } from './selection';
82
82
  export { FoirDecimal, decimal } from './decimal';
83
83
  export type { FoirDecimalDivOptions, FoirDecimalInput, FoirDecimalRounding, } from './decimal';
84
84
  export { FoirRequestError, FoirAuthDowngradedError, FoirSessionExpiredError, FOIR_VISITOR_IP_HEADER, stepUpRequired, } from '../transport/foir-http';
85
85
  export type { FoirEnv, FoirGraphQLErrorItem, FoirTokenProvider, FoirAuthDowngrade, FoirStepUp, FoirVisitor, FoirVisitorSource, } from '../transport/foir-http';
86
+ export { askAgent, uploadAgentFile, visitorToken } from './agents';
87
+ export type { AgentReply, AskAgentOptions, FoirAgentFile, FoirAgentFilesClient, FoirAgentsClient } from './agents';
@@ -140,6 +140,19 @@ export interface FoirSubjectOptions {
140
140
  */
141
141
  label?: (record: Readonly<Record<string, unknown>>, subject: FoirSubject) => string | null | undefined;
142
142
  }
143
+ /** One field of one record, as an element on the page was marked with it. */
144
+ export interface FoirFieldMark {
145
+ /** The record the field belongs to. */
146
+ subject: FoirSubject;
147
+ /** The field's key on the model, e.g. `summary`, or a nested path. */
148
+ field: string;
149
+ /** What to call the field on screen, when the host named it. */
150
+ label?: string;
151
+ }
152
+ /** What `FoirSubjectRegistry.field` returns: spread it onto the element. */
153
+ export interface FoirFieldMarkAttributes {
154
+ 'data-foir-field': string;
155
+ }
143
156
  export interface FoirSubjectRegistry {
144
157
  /**
145
158
  * Record one subject.
@@ -190,6 +203,38 @@ export interface FoirSubjectRegistry {
190
203
  * failure is reported rather than dropped (see `recordFoirSubjects`).
191
204
  */
192
205
  subscribe(listener: () => void): () => void;
206
+ /**
207
+ * Mark an element as showing one field of a record, so a widget can let the
208
+ * reader point at what they mean. Spread the answer onto the element:
209
+ *
210
+ * ```tsx
211
+ * <h1 {...subjects.field(page, 'title', { label: 'Title' })}>{page.title}</h1>
212
+ * ```
213
+ *
214
+ * The attribute is an opaque token, not the model, record or field: the
215
+ * registry holds what it stands for, as it holds every subject, so the page
216
+ * does not print the project's vocabulary. It is the same token for the same
217
+ * field wherever it is computed, so a server-rendered mark hydrates to the
218
+ * attribute the server wrote. Call it where the browser renders too: a mark
219
+ * only a server component made has nothing in the browser to resolve it.
220
+ *
221
+ * `label` is the only thing about the field a widget shows. Without one, it
222
+ * names the record and not the field.
223
+ */
224
+ field(subject: Pick<FoirSubject, 'modelKey' | 'recordId'> & {
225
+ label?: string;
226
+ }, field: string, options?: {
227
+ label?: string;
228
+ }): FoirFieldMarkAttributes;
229
+ /**
230
+ * The field a marked element points at, by its `data-foir-field` token.
231
+ * Undefined for a token this registry did not mark.
232
+ *
233
+ * `clear()` keeps the marks. A single-page app clears on navigation, and the
234
+ * next page may already have rendered its marks by then; a stale mark points
235
+ * at nothing on screen, so it is never offered.
236
+ */
237
+ pointedAt(token: string): FoirFieldMark | undefined;
193
238
  }
194
239
  /**
195
240
  * Build a registry.
@@ -50,6 +50,7 @@
50
50
  * round trip in front of every query for good reason; in front of a socket that
51
51
  * will live for minutes it is noise.
52
52
  */
53
+ import type { FoirAgentRunNode, FoirAgentRunStatus, FoirAgentThreadMessage } from './builtins';
53
54
  import type { FoirManifest } from './manifest';
54
55
  import type { FoirClientConfig } from './transport';
55
56
  import { type FoirSubscription, type FoirSubscriptionHandlers } from './ws-transport';
@@ -108,6 +109,40 @@ export interface FoirOperationExecutionEvent {
108
109
  /** Present on `status: failed` or `timed_out`. */
109
110
  errorJson?: unknown;
110
111
  }
112
+ /**
113
+ * `agentRun` — one event of an agent's run as it answers.
114
+ *
115
+ * TEXT and REASONING carry new text; NODE a step that started, changed or
116
+ * ended (a tool call, a held act, a child agent); MESSAGE a message kept on
117
+ * the thread; STATUS where the run now stands. The stream ends after a STATUS
118
+ * that is final.
119
+ */
120
+ export interface FoirAgentRunEvent {
121
+ /** Resume a dropped stream from the last seq seen. */
122
+ seq: string;
123
+ kind: 'TEXT' | 'REASONING' | 'NODE' | 'MESSAGE' | 'STATUS' | 'DATA';
124
+ text?: string | null;
125
+ messageId?: string | null;
126
+ node?: FoirAgentRunNode | null;
127
+ message?: FoirAgentThreadMessage | null;
128
+ status?: FoirAgentRunStatus | null;
129
+ answer?: string | null;
130
+ error?: string | null;
131
+ /** DATA: which of the agent's data parts, and the data, which fits its schema. */
132
+ dataName?: string | null;
133
+ data?: unknown;
134
+ /** STATUS, once a run of an agent with an output schema has completed: its typed answer. */
135
+ output?: unknown;
136
+ }
137
+ /** Options for `foir.ns.agents.onRun`. */
138
+ export type FoirAgentRunSubscribeOptions = FoirSubscriptionHandlers<FoirAgentRunEvent> & {
139
+ /** The run's id, as `startAgentRun` answered it. */
140
+ id: string;
141
+ /** Only events after this seq, to resume a dropped stream. */
142
+ after?: string;
143
+ /** A visitor with nobody signed in: the secret their browser keeps. */
144
+ visitorToken?: string;
145
+ };
111
146
  /** Options for `foir.ns.operations.onExecution`. */
112
147
  export type FoirExecutionSubscribeOptions = FoirSubscriptionHandlers<FoirOperationExecutionEvent> & {
113
148
  /** Execution id, as returned by `execute<Op>`. */
@@ -168,6 +203,9 @@ export type FoirNamespaceSubscriptions<NK> = NK extends 'notifications' ? {
168
203
  onJobProgress(handlers: FoirSubscriptionHandlers<FoirJobProgressEvent>): FoirSubscription;
169
204
  /** Stream one operation execution's progress until it reaches a terminal state. */
170
205
  onExecution(options: FoirExecutionSubscribeOptions): FoirSubscription;
206
+ } : NK extends 'agents' ? {
207
+ /** Follow an agent's run as it answers, until its final status. */
208
+ onRun(options: FoirAgentRunSubscribeOptions): FoirSubscription;
171
209
  } : {};
172
210
  /** Does this (address, property) pair name a subscription rather than an op? */
173
211
  export declare function isModelSubscriptionMethod(name: string): boolean;
@@ -725,4 +725,5 @@ export declare function executeFoirDocument<T>(config: FoirClientConfig, plan: F
725
725
  export declare function executeFoirRawDocument<T>(config: FoirClientConfig, document: string, mutation: boolean, variables?: Record<string, unknown>, options?: {
726
726
  strict?: boolean;
727
727
  }): Promise<T>;
728
+ export declare function renewBearerOnce(refresh: () => Promise<string | null>): Promise<string | null>;
728
729
  export {};
@@ -149,6 +149,7 @@ export declare class FoirSocketTransport {
149
149
  private disposed;
150
150
  private keepAlive;
151
151
  private retryTimer;
152
+ private renewedBearer;
152
153
  constructor(config: FoirRequestConfig);
153
154
  /**
154
155
  * Start a subscription.
@@ -167,6 +168,14 @@ export declare class FoirSocketTransport {
167
168
  private ensureConnected;
168
169
  private sendConnectionInit;
169
170
  private handleClose;
171
+ /**
172
+ * Reconnect after the socket's credential lapsed: with the bearer renewed
173
+ * through the client's `refreshBearer` where it has one, failing every
174
+ * subscription with `FoirSessionExpiredError` when the session is over. A
175
+ * client without one reconnects at once, presenting what its bearer
176
+ * provider gives now.
177
+ */
178
+ private reconnectWithARenewedBearer;
170
179
  private scheduleReconnect;
171
180
  private teardownSocket;
172
181
  private receive;
package/dist/data.js CHANGED
@@ -1380,6 +1380,8 @@ function printSubscriptionDocument(input) {
1380
1380
  }
1381
1381
 
1382
1382
  // src/data/ws-transport.ts
1383
+ var CLOSE_CREDENTIAL_ENDED = 4402;
1384
+ var CLOSE_FORBIDDEN = 4403;
1383
1385
  var GQL = {
1384
1386
  connectionInit: "connection_init",
1385
1387
  connectionAck: "connection_ack",
@@ -1414,6 +1416,9 @@ var FoirSocketTransport = class {
1414
1416
  disposed = false;
1415
1417
  keepAlive = null;
1416
1418
  retryTimer = null;
1419
+ // A bearer renewed after the socket's lapsed (4402), presented from the next
1420
+ // connect on in place of the client's own.
1421
+ renewedBearer = null;
1417
1422
  constructor(config) {
1418
1423
  if (config.dispatch !== void 0) {
1419
1424
  throw new FoirRequestError(
@@ -1486,12 +1491,14 @@ var FoirSocketTransport = class {
1486
1491
  socket.onmessage = (event) => this.receive(event.data);
1487
1492
  socket.onerror = () => {
1488
1493
  };
1489
- socket.onclose = () => this.handleClose();
1494
+ socket.onclose = (event) => this.handleClose(event);
1490
1495
  }
1491
1496
  async sendConnectionInit(socket) {
1492
1497
  this.connecting = false;
1493
1498
  try {
1494
- const { headers } = await resolveFoirAuth(this.config);
1499
+ const { headers } = await resolveFoirAuth(
1500
+ this.renewedBearer ? { ...this.config, bearerToken: this.renewedBearer } : this.config
1501
+ );
1495
1502
  const { "Content-Type": _contentType, ...params } = headers;
1496
1503
  const extra = await this.config.connectionParams?.() ?? {};
1497
1504
  this.send(socket, {
@@ -1505,19 +1512,53 @@ var FoirSocketTransport = class {
1505
1512
  this.teardownSocket(true);
1506
1513
  }
1507
1514
  }
1508
- handleClose() {
1515
+ handleClose(event) {
1516
+ const code = event?.code;
1509
1517
  const wasAcknowledged = this.acknowledged;
1510
1518
  this.teardownSocket(false);
1511
1519
  if (this.disposed || this.registrations.size === 0) return;
1512
1520
  for (const registration of this.registrations.values()) {
1513
1521
  registration.sent = false;
1514
1522
  }
1523
+ if (code === CLOSE_FORBIDDEN) {
1524
+ this.failAll(
1525
+ new FoirRequestError(
1526
+ "[foir] the subscription socket was refused where it asked to stand: the signed-in user is no member of the organization this client names"
1527
+ )
1528
+ );
1529
+ return;
1530
+ }
1531
+ if (code === CLOSE_CREDENTIAL_ENDED) {
1532
+ void this.reconnectWithARenewedBearer();
1533
+ return;
1534
+ }
1515
1535
  this.scheduleReconnect(
1516
1536
  new FoirRequestError(
1517
1537
  wasAcknowledged ? "[foir] the subscription socket closed and could not be re-established" : "[foir] the subscription socket closed before the credential was accepted"
1518
1538
  )
1519
1539
  );
1520
1540
  }
1541
+ /**
1542
+ * Reconnect after the socket's credential lapsed: with the bearer renewed
1543
+ * through the client's `refreshBearer` where it has one, failing every
1544
+ * subscription with `FoirSessionExpiredError` when the session is over. A
1545
+ * client without one reconnects at once, presenting what its bearer
1546
+ * provider gives now.
1547
+ */
1548
+ async reconnectWithARenewedBearer() {
1549
+ const refresh = this.config.refreshBearer;
1550
+ if (refresh) {
1551
+ const token = await renewBearerOnce(refresh).catch(() => null);
1552
+ if (this.disposed) return;
1553
+ if (!token) {
1554
+ this.failAll(new FoirSessionExpiredError());
1555
+ return;
1556
+ }
1557
+ this.renewedBearer = token;
1558
+ }
1559
+ this.attempt = 0;
1560
+ this.ensureConnected();
1561
+ }
1521
1562
  scheduleReconnect(error) {
1522
1563
  if (this.disposed || this.registrations.size === 0) return;
1523
1564
  if (this.attempt >= this.policy.maxAttempts) {
@@ -1741,7 +1782,8 @@ function describe(payload) {
1741
1782
  var MODEL_SUBSCRIPTION_METHODS = /* @__PURE__ */ new Set(["onChange"]);
1742
1783
  var NAMESPACE_SUBSCRIPTION_METHODS = {
1743
1784
  notifications: /* @__PURE__ */ new Set(["onNotification"]),
1744
- operations: /* @__PURE__ */ new Set(["onJobProgress", "onExecution"])
1785
+ operations: /* @__PURE__ */ new Set(["onJobProgress", "onExecution"]),
1786
+ agents: /* @__PURE__ */ new Set(["onRun"])
1745
1787
  };
1746
1788
  function isModelSubscriptionMethod(name) {
1747
1789
  return MODEL_SUBSCRIPTION_METHODS.has(name);
@@ -1825,6 +1867,25 @@ var FoirSubscriptionRouter = class {
1825
1867
  { ...handlers, onData: (event) => handlers.onData(executionEventWithEnumStatus(event)) }
1826
1868
  );
1827
1869
  }
1870
+ if (namespace === "agents" && method === "onRun") {
1871
+ const id = input.id;
1872
+ if (typeof id !== "string" || id === "") {
1873
+ throw new FoirRequestError("[foir] onRun requires a run `id`");
1874
+ }
1875
+ const args = [{ name: "id", type: "String!" }];
1876
+ const variables = { id };
1877
+ for (const name of ["after", "visitorToken"]) {
1878
+ const v = input[name];
1879
+ if (typeof v === "string" && v !== "") {
1880
+ args.push({ name, type: "String" });
1881
+ variables[name] = v;
1882
+ }
1883
+ }
1884
+ return this.open(
1885
+ { field: "agentRun", operationName: "FoirAgentRun", args, variables },
1886
+ handlers
1887
+ );
1888
+ }
1828
1889
  throw new FoirRequestError(`[foir] ns.${namespace}.${method} is not a subscription`);
1829
1890
  }
1830
1891
  /**
@@ -1908,12 +1969,24 @@ function normalise(value) {
1908
1969
  }
1909
1970
 
1910
1971
  // src/data/subjects.ts
1972
+ function markToken(modelKey, recordId, field) {
1973
+ const text = `${modelKey}\0${recordId}\0${field}`;
1974
+ let fnv = 2166136261;
1975
+ let djb = 5381;
1976
+ for (let i = 0; i < text.length; i++) {
1977
+ const code = text.charCodeAt(i);
1978
+ fnv = Math.imul(fnv ^ code, 16777619) >>> 0;
1979
+ djb = (Math.imul(djb, 33) ^ code) >>> 0;
1980
+ }
1981
+ return `f${fnv.toString(36)}${djb.toString(36)}`;
1982
+ }
1911
1983
  var keyOf = (modelKey, recordId) => `${modelKey}\0${recordId}`;
1912
1984
  function createFoirSubjectRegistry(options = {}) {
1913
1985
  const held = /* @__PURE__ */ new Map();
1914
1986
  const listeners = /* @__PURE__ */ new Set();
1915
1987
  const pinned = { subject: null };
1916
1988
  const latest = { key: null };
1989
+ const marks = /* @__PURE__ */ new Map();
1917
1990
  const notify = () => {
1918
1991
  const failures = [];
1919
1992
  for (const listener of [...listeners]) {
@@ -1984,6 +2057,25 @@ function createFoirSubjectRegistry(options = {}) {
1984
2057
  return () => {
1985
2058
  listeners.delete(listener);
1986
2059
  };
2060
+ },
2061
+ field(subject, field, fieldOptions = {}) {
2062
+ if (!subject.modelKey || !subject.recordId || !field) {
2063
+ throw new FoirRequestError(
2064
+ "[foir] a field mark needs a modelKey, a recordId and a field \u2014 a mark missing one points at nothing a note can be filed against"
2065
+ );
2066
+ }
2067
+ const token = markToken(subject.modelKey, subject.recordId, field);
2068
+ const mark = {
2069
+ subject: { modelKey: subject.modelKey, recordId: subject.recordId },
2070
+ field
2071
+ };
2072
+ if (subject.label) mark.subject.label = subject.label;
2073
+ if (fieldOptions.label) mark.label = fieldOptions.label;
2074
+ marks.set(token, mark);
2075
+ return { "data-foir-field": token };
2076
+ },
2077
+ pointedAt(token) {
2078
+ return marks.get(token);
1987
2079
  }
1988
2080
  };
1989
2081
  }
@@ -2675,6 +2767,119 @@ var FoirDecimal = class _FoirDecimal {
2675
2767
  function decimal(value) {
2676
2768
  return new FoirDecimal(value);
2677
2769
  }
2770
+
2771
+ // src/data/agents.ts
2772
+ async function uploadAgentFile(foir, options) {
2773
+ const agents = foir.ns.agents;
2774
+ if (typeof agents?.createAgentAttachment !== "function") {
2775
+ throw new Error("[foir] uploadAgentFile: this client sends no agent files. Its key runs none of the project's agents, or the project has released none.");
2776
+ }
2777
+ const { blob, mediaType, filename } = fileParts(options.file);
2778
+ const up = await agents.createAgentAttachment({
2779
+ agent: options.agent,
2780
+ filename,
2781
+ mediaType,
2782
+ size: blob.size,
2783
+ ...options.visitorToken ? { visitorToken: options.visitorToken } : {}
2784
+ });
2785
+ const put = await (options.fetch ?? fetch)(up.uploadUrl, { method: "PUT", headers: { "content-type": mediaType }, body: blob });
2786
+ if (!put.ok) throw new Error(`[foir] uploadAgentFile: ${filename} was not uploaded (${put.status}).`);
2787
+ return up.attachment;
2788
+ }
2789
+ function fileParts(file) {
2790
+ if (typeof Blob !== "undefined" && file instanceof Blob) {
2791
+ const name = file.name;
2792
+ return { blob: file, mediaType: file.type, filename: name || "file" };
2793
+ }
2794
+ const { data, mediaType, filename } = file;
2795
+ const blob = data instanceof Blob ? data : new Blob([data], { type: mediaType });
2796
+ return { blob, mediaType, filename: filename || "file" };
2797
+ }
2798
+ var FINAL = /* @__PURE__ */ new Set(["COMPLETED", "FAILED", "SKIPPED", "CANCELLED", "HELD"]);
2799
+ async function askAgent(foir, options) {
2800
+ const agents = foir.ns.agents;
2801
+ if (typeof agents?.startAgentRun !== "function" || typeof agents.onRun !== "function") {
2802
+ throw new Error(
2803
+ "[foir] askAgent: this client offers no agents. Its key runs none of the project's agents (agents:execute), or the project has released none."
2804
+ );
2805
+ }
2806
+ const { onText, onEvent, signal, files, ...args } = options;
2807
+ if (files?.length) {
2808
+ const sent = await Promise.all(files.map((file) => uploadAgentFile(foir, { agent: args.agent, file, visitorToken: args.visitorToken })));
2809
+ args.attachmentIds = [...args.attachmentIds ?? [], ...sent.map((a) => a.id)];
2810
+ }
2811
+ const run = await agents.startAgentRun(args);
2812
+ const reply = {
2813
+ runId: run.id,
2814
+ threadId: run.threadId ?? null,
2815
+ status: run.status,
2816
+ answer: run.answer ?? "",
2817
+ error: run.error ?? null,
2818
+ ...run.output === void 0 || run.output === null ? {} : { output: run.output }
2819
+ };
2820
+ if (FINAL.has(run.status) && run.status !== "HELD") return reply;
2821
+ let written = "";
2822
+ return new Promise((resolve, reject) => {
2823
+ let settled = false;
2824
+ const finish = (sub2, err) => {
2825
+ if (settled) return;
2826
+ settled = true;
2827
+ sub2?.unsubscribe();
2828
+ if (err !== void 0) reject(err);
2829
+ else resolve({ ...reply, answer: reply.answer || written });
2830
+ };
2831
+ let sub = null;
2832
+ sub = agents.onRun({
2833
+ id: run.id,
2834
+ visitorToken: args.visitorToken,
2835
+ onData: (event) => {
2836
+ onEvent?.(event);
2837
+ if (event.kind === "TEXT" && event.text) {
2838
+ written += event.text;
2839
+ onText?.(event.text);
2840
+ }
2841
+ if (event.kind === "STATUS" && event.status) {
2842
+ reply.status = event.status;
2843
+ if (event.answer) reply.answer = event.answer;
2844
+ if (event.error) reply.error = event.error;
2845
+ if (event.output !== void 0 && event.output !== null) reply.output = event.output;
2846
+ if (FINAL.has(event.status)) finish(sub);
2847
+ }
2848
+ },
2849
+ onError: (err) => finish(sub, err),
2850
+ onComplete: () => finish(sub)
2851
+ });
2852
+ signal?.addEventListener("abort", () => finish(sub), { once: true });
2853
+ });
2854
+ }
2855
+ var VISITOR_KEY = "foir:agents:visitor";
2856
+ function visitorToken(storage = browserStorage(), key = VISITOR_KEY) {
2857
+ try {
2858
+ const kept = storage?.getItem(key);
2859
+ if (kept) return kept;
2860
+ } catch {
2861
+ }
2862
+ const made = randomSecret();
2863
+ try {
2864
+ storage?.setItem(key, made);
2865
+ } catch {
2866
+ }
2867
+ return made;
2868
+ }
2869
+ function browserStorage() {
2870
+ try {
2871
+ return typeof localStorage === "undefined" ? null : localStorage;
2872
+ } catch {
2873
+ return null;
2874
+ }
2875
+ }
2876
+ function randomSecret() {
2877
+ const bytes = new Uint8Array(24);
2878
+ globalThis.crypto.getRandomValues(bytes);
2879
+ let out = "";
2880
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
2881
+ return out;
2882
+ }
2678
2883
  export {
2679
2884
  FOIR_VISITOR_IP_HEADER,
2680
2885
  FoirAuthDowngradedError,
@@ -2683,6 +2888,7 @@ export {
2683
2888
  FoirSchemaDriftError,
2684
2889
  FoirSessionExpiredError,
2685
2890
  FoirSocketTransport,
2891
+ askAgent,
2686
2892
  createFoirClient,
2687
2893
  createFoirClientFromEnv,
2688
2894
  createFoirSubjectRegistry,
@@ -2696,5 +2902,7 @@ export {
2696
2902
  planFoirNamespaceCall,
2697
2903
  printSubscriptionDocument,
2698
2904
  recordFoirSubjects,
2699
- stepUpRequired
2905
+ stepUpRequired,
2906
+ uploadAgentFile,
2907
+ visitorToken
2700
2908
  };