@foir/sdk 0.29.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.
package/CHANGELOG.md CHANGED
@@ -108,10 +108,94 @@ nobody asking for `@foir/sdk` gets it early. The push that carries the commit to
108
108
 
109
109
  Changes on `dev` that have not been published to npm.
110
110
 
111
+ ### Added
112
+
113
+ - **Tags in a message** (`@foir/sdk/ai`, `@foir/sdk/data`): a message can
114
+ point at a record, a field, a model, a person, a file, an operation, an
115
+ agent, a hook, a schedule, an organisation or a note, by id. The `useChat`
116
+ transport sends each `data-tag` part of the message as a tag, and
117
+ `startAgentRun` (and so `askAgent`) takes `tags`, typed `FoirMessageTag`.
118
+ The agent reads what a tag points at with its own tools, as the caller.
119
+ - **Images and PDFs in a message** (`@foir/sdk/ai`, `@foir/sdk/data`): the
120
+ `useChat` transport sends the files of `sendMessage({ text, files })`, and
121
+ `askAgent` takes `files`. Each is uploaded first with the new
122
+ `uploadAgentFile` (or the `createAgentAttachment` mutation) and sent by its
123
+ id in `startAgentRun`'s `attachmentIds`. `foirChatMessages` reads a sent
124
+ file back as a `file` part with a link that lasts an hour. An image (PNG,
125
+ JPEG, GIF or WebP) may be up to 5 MB and a PDF up to 10 MB, at most five a
126
+ message; a visitor may send images only, 20 a day.
127
+ - **Field marks** (`@foir/sdk/data`): `subjects.field(record, 'title', { label })`
128
+ returns `{ 'data-foir-field': token }` to spread onto the element that shows
129
+ that field, and `subjects.pointedAt(token)` reads it back as a
130
+ `FoirFieldMark`. The token is opaque, so the page prints none of the model,
131
+ record or field, and the same field gets the same token on the server and in
132
+ the browser.
133
+ - **`<FoirFeedback pointing>`** (`@foir/sdk/react`): the reader points at the
134
+ part of the page they mean, by clicking a marked field or by Tab and Enter,
135
+ and the note is filed against that record and names the field
136
+ (`anchorField`). A page showing two records files each comment against the
137
+ right one. A visitor may name a field they can read; the data API refuses
138
+ any other. Offered only while the page shows a marked field. `writeNote`
139
+ takes the anchor from this release of the data API; a client generated
140
+ before it needs `foir types` re-run before it can send one.
141
+ - **`<FoirFeedback launcher="bottom-right" | "bottom-left">`**: the button is
142
+ fixed in that corner of the viewport, and the inline panel opens above it.
143
+ - **`@foir/sdk/ai`**: `foirChatTransport({ client, agent })` puts a project's
144
+ agent behind the AI SDK's `useChat`, with no backend of the app's own. Text
145
+ and reasoning stream into their parts, each step the agent takes is a
146
+ `dynamic-tool` part, and an act held for the person is the AI SDK's
147
+ approval step, decided with `addToolApprovalResponse`. The chat keeps its
148
+ thread in the browser's storage; `foirChatMessages` reads a thread back as
149
+ `useChat`'s messages. `ai` (7.x) is an optional peer, which
150
+ `@foir/sdk/ai` needs. Data an agent sends the app arrives as `data-<name>`
151
+ parts.
152
+ - **Tools of the page** (`@foir/sdk/ai`): `foirChatTransport({ pageTools })`
153
+ defines the page's own tools, each with a `description`, an `inputSchema`
154
+ (a Zod schema, a Standard Schema or `jsonSchema()`) and an `execute`, as an
155
+ AI SDK `tool()` has. They are sent with every message to an agent that
156
+ allows the page's tools. When the agent calls one, the transport runs
157
+ `execute` and the run carries on in the same answer with what it returned;
158
+ what it throws is answered as `{ error }`. A tool with no `execute` is a
159
+ tool part the app answers with `addToolOutput`. `startAgentRun` takes
160
+ `pageTools`, typed `FoirPageToolOffer`.
161
+ - **Typed answers** (`@foir/sdk/data`): `askAgent` resolves with `output`, the
162
+ value a run of an agent with an output schema answers with. `FoirAgentRun`
163
+ carries `output`, and a run's events carry `DATA` (a data part sent to the
164
+ app) and the final `STATUS` event's `output`. A run's nodes can be
165
+ `PAGE_TOOL` (a tool the page offered, held until `answerPageTool`) and
166
+ `DATA`.
167
+
168
+ ### Fixed
169
+
170
+ - **`CodeFlowConfig.issuer` names Foir's issuer as `https://api.foir.dev`** in
171
+ its description, where it said `https://api.foir.io`. The comments of
172
+ `@foir/sdk/auth` and `<SignIn>` call the hosted page hosted sign-in.
173
+ - **`foirChatTransport` and `foirChatMessages` take any client
174
+ `createFoirClient` returns**, typed by the project's schema or not, as
175
+ `askAgent` does. The documented example passed such a client and did not
176
+ typecheck. A client whose key runs no agent is refused when the first
177
+ message is sent, saying so.
178
+ - **A `useChat` answer that is cancelled stops following its run.** The
179
+ transport kept the run's subscription open and wrote each later event to a
180
+ stream nobody read, which threw. A page loaded again still picks the answer
181
+ up from the stored run with `resume`.
182
+
111
183
  ## [0.29.0] — 2026-10-21
112
184
 
113
185
  ### Added
114
186
 
187
+ - **Agents** (`@foir/sdk/data`): a project's agents through the data API.
188
+ `foir.ns.agents.startAgentRun`, `cancelAgentRun`, `agentRun`, `agentThreads`
189
+ and the approvals verbs are offered where the key runs some agent, and
190
+ `foir.ns.agents.onRun({ id })` follows a run as it is written, from its start
191
+ so nothing is missed. `askAgent(foir, { agent, message, threadId, onText })`
192
+ starts a run and resolves with the answer, the thread to go on with and how
193
+ the run ended; `visitorToken()` is a visitor's own secret, kept in the
194
+ browser, which makes a conversation theirs. Types: `FoirAgentRun`,
195
+ `FoirAgentRunEvent`, `FoirAgentRunNode`, `FoirAgentRunStatus`,
196
+ `FoirAgentThread`, `FoirAgentThreadMessage`, `FoirAgentApproval`,
197
+ `FoirMcpConnection`, `FoirStartAgentRunArgs`, `FoirAgentsClient`,
198
+ `AskAgentOptions`, `AgentReply`.
115
199
  - **`AuthorizedApp` says whose app it is**: `clientLogoUrl`, `clientLogoAlt`
116
200
  and `clientHomepageUrl`, where the app set them.
117
201
  - **`beginSignIn` and `completeSignIn`** (`@foir/sdk/auth`): the OpenID Connect
@@ -0,0 +1,190 @@
1
+ /**
2
+ * A Fóir agent behind the AI SDK's `useChat`.
3
+ *
4
+ * ```tsx
5
+ * import { useChat } from '@ai-sdk/react';
6
+ * import { lastAssistantMessageIsCompleteWithApprovalResponses } from 'ai';
7
+ * import { createFoirClient } from '@foir/sdk/data';
8
+ * import { foirChatTransport } from '@foir/sdk/ai';
9
+ *
10
+ * const foir = createFoirClient({ apiUrl, apiKey });
11
+ * const transport = foirChatTransport({ client: foir, agent: 'support' });
12
+ *
13
+ * const { messages, sendMessage, addToolApprovalResponse } = useChat({
14
+ * transport,
15
+ * sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
16
+ * });
17
+ * ```
18
+ *
19
+ * Each message starts a run of the agent on Fóir. Nothing runs in the app's
20
+ * own backend, and the app holds no model key. The run's events arrive as the
21
+ * message parts `useChat` renders: text and reasoning as they are written, and
22
+ * each step the agent takes as a tool part. An act the agent holds for the
23
+ * person's decision is the AI SDK's approval step: `addToolApprovalResponse`
24
+ * decides it, and the run carries on. An act held for the project's team shows
25
+ * as waiting, since the person cannot decide it.
26
+ *
27
+ * Data the agent sends the app's own components arrives as `data-<name>`
28
+ * parts. The page's own tools are defined once, in `pageTools`, and sent with
29
+ * each message to an agent that allows them:
30
+ *
31
+ * ```ts
32
+ * const transport = foirChatTransport({
33
+ * client: foir,
34
+ * agent: 'shop',
35
+ * pageTools: {
36
+ * show_product: {
37
+ * description: 'Open a product on the page.',
38
+ * inputSchema: z.object({ id: z.string() }),
39
+ * execute: ({ id }) => {
40
+ * router.push(`/products/${id}`);
41
+ * return { shown: id };
42
+ * },
43
+ * },
44
+ * },
45
+ * });
46
+ * ```
47
+ *
48
+ * The transport runs a tool's `execute` when the agent calls it, and the run
49
+ * carries on with what it returned. A tool with no `execute` is a tool part
50
+ * waiting for its output: answer it in `onToolCall` with `addToolOutput`.
51
+ *
52
+ * Fóir keeps the conversation. A chat goes on in its thread for as long as
53
+ * `threads` keeps the thread's id (the browser's storage by default), and a
54
+ * visitor's thread is theirs by the secret their browser keeps.
55
+ * `foirChatMessages` reads a thread back as `useChat`'s messages.
56
+ */
57
+ import type { ChatTransport, FlexibleSchema, UIMessage, UIMessageChunk } from 'ai';
58
+ import type { FoirAgentAttachmentUpload, FoirAgentRun, FoirAgentRunNode, FoirAgentThread, FoirCreateAgentAttachmentArgs, FoirStartAgentRunArgs } from '../data/builtins';
59
+ import type { FoirAgentRunEvent, FoirAgentRunSubscribeOptions } from '../data/subscriptions';
60
+ import type { FoirSubscription } from '../data/ws-transport';
61
+ /** What the transport needs of a client: the agents namespace a key that runs an agent is given. */
62
+ export interface FoirChatClient {
63
+ ns: {
64
+ agents: {
65
+ startAgentRun(args: FoirStartAgentRunArgs): Promise<FoirAgentRun>;
66
+ onRun(options: FoirAgentRunSubscribeOptions): FoirSubscription;
67
+ decideAgentApproval(args: {
68
+ id: string;
69
+ approve: boolean;
70
+ reason?: string;
71
+ }): Promise<FoirAgentRunNode | null>;
72
+ answerPageTool(args: {
73
+ id: string;
74
+ output: unknown;
75
+ visitorToken?: string;
76
+ }): Promise<FoirAgentRunNode | null>;
77
+ agentThread(args: {
78
+ id: string;
79
+ visitorToken?: string;
80
+ }): Promise<FoirAgentThread | null>;
81
+ createAgentAttachment(args: FoirCreateAgentAttachmentArgs): Promise<FoirAgentAttachmentUpload>;
82
+ };
83
+ };
84
+ }
85
+ /**
86
+ * A tool of the app's own page, which the agent may call while it answers. An
87
+ * AI SDK `tool({ ... })` is one.
88
+ */
89
+ export interface FoirPageTool<INPUT = any> {
90
+ /** What the tool does, as the model is told it. At most 1000 characters. */
91
+ description?: string;
92
+ /**
93
+ * The tool's input: a Zod schema, a Standard Schema, or a JSON Schema
94
+ * through the AI SDK's `jsonSchema()`. It must describe an object. Left
95
+ * out, the tool takes no input.
96
+ */
97
+ inputSchema?: FlexibleSchema<INPUT>;
98
+ /**
99
+ * Does what the tool does on the page, and answers what it did: JSON, as
100
+ * the model is told it, at most 64 KB. What it throws is answered as
101
+ * `{ error }`. Left out, the call waits for `addToolOutput`.
102
+ */
103
+ execute?(input: INPUT, options: {
104
+ toolCallId: string;
105
+ messages: unknown[];
106
+ abortSignal?: AbortSignal;
107
+ }): unknown;
108
+ }
109
+ /** Where a chat's thread id is kept between page loads. */
110
+ export type FoirThreadStore = Pick<Storage, 'getItem' | 'setItem'>;
111
+ export interface FoirChatTransportOptions {
112
+ /**
113
+ * A Fóir client whose key runs the agent: `createFoirClient`'s, typed by
114
+ * the project's schema or not. A client that offers no agents is refused
115
+ * when the first message is sent, saying so.
116
+ */
117
+ client: FoirChatClient | {
118
+ readonly ns: object;
119
+ };
120
+ /** The agent's key. */
121
+ agent: string;
122
+ /**
123
+ * A visitor with nobody signed in: the secret their browser keeps. Defaults
124
+ * to the one `visitorToken()` keeps; pass `null` for a signed-in person.
125
+ */
126
+ visitorToken?: string | null;
127
+ /** Where each chat's thread id is kept. The browser's storage by default; `null` keeps it for the page. */
128
+ threads?: FoirThreadStore | null;
129
+ /**
130
+ * The page's own tools, by name: lowercase letters, digits and _. Each is
131
+ * sent with every message, and the agent must allow the page's tools. At
132
+ * most 20.
133
+ */
134
+ pageTools?: Record<string, FoirPageTool>;
135
+ }
136
+ /**
137
+ * One run's events as `useChat`'s chunks. A run outlives a stream: it stops
138
+ * for a decision and carries on once it is made, so what the person has
139
+ * already been shown (the steps, the last event) is kept per run, and each
140
+ * stream only opens and closes its own text.
141
+ */
142
+ export declare class FoirRunChunks {
143
+ private readonly runId;
144
+ private readonly runs;
145
+ private readonly tools;
146
+ /** The last event shown, to follow the run again from. */
147
+ after: string;
148
+ private textId;
149
+ private reasoningId;
150
+ private opened;
151
+ /** The run stopped for this stream's followers: it ended, or holds an act. */
152
+ stopped: boolean;
153
+ /** Calls to the page's tools waiting for the app's `addToolOutput`. */
154
+ readonly pageCalls: Set<string>;
155
+ /** Calls to the page's tools the transport runs and answers itself, until the run hears them. */
156
+ readonly answering: Set<string>;
157
+ /** Calls the transport is to run: taken with `takeCalls`. */
158
+ private toRun;
159
+ /** Acts held for a decision, by anyone. */
160
+ private readonly held;
161
+ /** `runs` names the page's tools the transport runs itself. */
162
+ constructor(runId: string, runs?: (tool: string) => boolean);
163
+ /** The calls to the page's tools that arrived for the transport to run. */
164
+ takeCalls(): FoirAgentRunNode[];
165
+ /** Starts a new stream of the same run, as after a decision. */
166
+ resume(): void;
167
+ private open;
168
+ private closeText;
169
+ private closeReasoning;
170
+ /** The chunks an event makes. `fresh` is a stream that starts a new message. */
171
+ of(event: FoirAgentRunEvent, fresh?: boolean): UIMessageChunk[];
172
+ private toolChunks;
173
+ /**
174
+ * A call to one of the page's tools. The transport runs one it has an
175
+ * `execute` for and shows what it answered; any other waits for the app's
176
+ * `addToolOutput`. One the page did not answer in time is an error.
177
+ */
178
+ private pageToolChunks;
179
+ }
180
+ /** The transport `useChat` sends through: each message is a run of a Fóir agent. */
181
+ export declare function foirChatTransport(options: FoirChatTransportOptions): ChatTransport<UIMessage>;
182
+ /**
183
+ * A thread read back as `useChat`'s messages, to open a chat where it was left.
184
+ * Fóir keeps each message's parts as the run wrote them.
185
+ */
186
+ export declare function foirChatMessages(client: FoirChatClient | {
187
+ readonly ns: object;
188
+ }, threadId: string, options?: {
189
+ visitorToken?: string | null;
190
+ }): Promise<UIMessage[]>;