akanjs 3.0.0-beta.4 → 3.0.0-beta.6
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/common/types.ts +5 -1
- package/fetch/client/fetchClient.ts +20 -2
- package/fetch/client/httpClient.ts +4 -0
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +1 -1
- package/server/di/diLifecycle.ts +5 -1
- package/service/predefinedAdaptor/anthropicLlm.ts +7 -1
- package/service/predefinedAdaptor/openaiDialect.ts +3 -2
- package/signal/middleware.ts +82 -47
- package/signal/serializer/fetch.serializer.ts +1 -0
- package/signal/signalContext.ts +8 -1
- package/signal/types.ts +24 -0
- package/types/common/types.d.ts +5 -1
- package/types/fetch/client/fetchClient.d.ts +8 -1
- package/types/fetch/client/httpClient.d.ts +2 -0
- package/types/service/predefinedAdaptor/anthropicLlm.d.ts +6 -0
- package/types/service/predefinedAdaptor/openaiDialect.d.ts +1 -1
- package/types/signal/middleware.d.ts +19 -10
- package/types/signal/signalContext.d.ts +6 -1
- package/types/signal/types.d.ts +24 -0
- package/types/ui/Agent/Attach.d.ts +1 -1
- package/types/ui/Agent/Chat.d.ts +4 -3
- package/types/ui/Loading/Spin.d.ts +19 -3
- package/types/ui/index.d.ts +1 -0
- package/ui/Agent/Attach.tsx +3 -3
- package/ui/Agent/Chat.tsx +4 -3
- package/ui/Agent/sessionHistory.ts +5 -2
- package/ui/Loading/Spin.tsx +24 -4
- package/ui/index.ts +1 -0
- package/vendor/use-agentic/Compaction.ts +3 -1
package/common/types.ts
CHANGED
|
@@ -5,7 +5,11 @@ export interface FetchPolicy<Returns = unknown> {
|
|
|
5
5
|
onError?: (error: string) => void;
|
|
6
6
|
token?: string;
|
|
7
7
|
partial?: string[];
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Milliseconds before this call is abandoned, `false` to wait as long as the runtime will. Overrides the
|
|
10
|
+
* endpoint's declared `timeout`, which overrides the client's own default.
|
|
11
|
+
*/
|
|
12
|
+
timeout?: number | false;
|
|
9
13
|
/**
|
|
10
14
|
* A `pubsub` subscription only: called after the room has been resubscribed following a dropped connection.
|
|
11
15
|
*
|
|
@@ -173,6 +173,13 @@ export class FetchClient {
|
|
|
173
173
|
}
|
|
174
174
|
: signal;
|
|
175
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* The budget for every call that neither names one nor is served by an endpoint declaring one. `false` waits
|
|
178
|
+
* as long as the runtime will, which is the browser's own limit — minutes.
|
|
179
|
+
*/
|
|
180
|
+
setTimeout(timeout?: number | false) {
|
|
181
|
+
this.http.setTimeout(timeout);
|
|
182
|
+
}
|
|
176
183
|
setErrorConstructor(ErrorCls?: ErrorConstructor) {
|
|
177
184
|
this.ErrorCls = ErrorCls;
|
|
178
185
|
this.http.setErrorConstructor(ErrorCls);
|
|
@@ -316,8 +323,9 @@ export class FetchClient {
|
|
|
316
323
|
const url = FetchClient.makeHttpUrl(key, endpoint, prefix, argMap);
|
|
317
324
|
const headers = this.#makeAuthHeaders(option);
|
|
318
325
|
const baseUrl = option?.origin;
|
|
326
|
+
const timeout = option?.timeout ?? endpoint.timeout;
|
|
319
327
|
|
|
320
|
-
const requestQuery = () => this.http.get(url, { headers, baseUrl });
|
|
328
|
+
const requestQuery = () => this.http.get(url, { headers, baseUrl, timeout });
|
|
321
329
|
|
|
322
330
|
const claim = baseUrl
|
|
323
331
|
? { value: requestQuery(), owned: true }
|
|
@@ -339,6 +347,7 @@ export class FetchClient {
|
|
|
339
347
|
const response = await this.http.send(endpoint.method ?? "POST", url, body, {
|
|
340
348
|
headers: this.#makeAuthHeaders(option),
|
|
341
349
|
baseUrl: option?.origin,
|
|
350
|
+
timeout: option?.timeout ?? endpoint.timeout,
|
|
342
351
|
});
|
|
343
352
|
const parsedReturn = parseReturn(response, { crystalize: option?.crystalize ?? true });
|
|
344
353
|
return parsedReturn;
|
|
@@ -827,7 +836,15 @@ export class FetchClient {
|
|
|
827
836
|
connect = false,
|
|
828
837
|
base,
|
|
829
838
|
Err,
|
|
830
|
-
|
|
839
|
+
timeout,
|
|
840
|
+
}: {
|
|
841
|
+
origin?: string;
|
|
842
|
+
connect?: boolean;
|
|
843
|
+
base?: FetchProxy;
|
|
844
|
+
Err?: ErrorConstructor;
|
|
845
|
+
/** This app's own default request budget, for calls no endpoint and no caller gave one. */
|
|
846
|
+
timeout?: number | false;
|
|
847
|
+
} = {},
|
|
831
848
|
): {
|
|
832
849
|
sig: ClientSignalMap<SigType>;
|
|
833
850
|
fetch: SigType["fetch"];
|
|
@@ -838,6 +855,7 @@ export class FetchClient {
|
|
|
838
855
|
const proxy =
|
|
839
856
|
shared ??
|
|
840
857
|
FetchClient.#makeProxy<unknown, Record<string, SliceMeta>>(new FetchClient(origin, {}, serializedSignal, Err));
|
|
858
|
+
if (timeout !== undefined) proxy.instance.setTimeout(timeout);
|
|
841
859
|
if (connect) proxy.instance.connect();
|
|
842
860
|
const sig = {} as any;
|
|
843
861
|
Object.entries(serializedSignal).forEach(([refName, serializedSignal]) => {
|
|
@@ -49,6 +49,10 @@ export class HttpClient {
|
|
|
49
49
|
setErrorConstructor(ErrorCls?: ErrorConstructor) {
|
|
50
50
|
this.ErrorCls = ErrorCls;
|
|
51
51
|
}
|
|
52
|
+
/** The budget every call that names none takes. `false` waits as long as the runtime will. */
|
|
53
|
+
setTimeout(timeout?: number | false) {
|
|
54
|
+
this.#timeout = timeout;
|
|
55
|
+
}
|
|
52
56
|
#resolveBaseUrl(baseUrl?: string) {
|
|
53
57
|
return (baseUrl ?? this.baseUrl).replace(/\/$/, "");
|
|
54
58
|
}
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/server/di/diLifecycle.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { agentTurnConstant, agentTurnDocument } from "../../signal/agentTurn";
|
|
|
17
17
|
import { Base, BaseEndpoint, BaseInternal } from "../../signal/base.signal";
|
|
18
18
|
import type { Endpoint } from "../../signal/endpoint";
|
|
19
19
|
import type { Internal } from "../../signal/internal";
|
|
20
|
-
import { Logging, type MiddlewareCls } from "../../signal/middleware";
|
|
20
|
+
import { Cache, Logging, type MiddlewareCls, Timeout } from "../../signal/middleware";
|
|
21
21
|
import type { ServerSignal, ServerSignalCls } from "../../signal/serverSignal";
|
|
22
22
|
import { SignalRegistry } from "../../signal/signalRegistry";
|
|
23
23
|
import type { AkanLib, DatabaseModule, ScalarModule, ServiceModule } from "../akanLib";
|
|
@@ -126,6 +126,10 @@ export class DiLifecycle {
|
|
|
126
126
|
: null;
|
|
127
127
|
if (frameworkAgent) this.#service.set("agent", frameworkAgent);
|
|
128
128
|
this.#middleware.set(Logging.refName, Logging);
|
|
129
|
+
|
|
130
|
+
this.#middleware.set(Timeout.refName, Timeout);
|
|
131
|
+
|
|
132
|
+
this.#middleware.set(Cache.refName, Cache);
|
|
129
133
|
const defaultOption = createDefaultAkanOption();
|
|
130
134
|
defaultOption.getMiddlewares().forEach((middleware) => {
|
|
131
135
|
this.#middleware.set(middleware.refName, middleware);
|
|
@@ -297,9 +297,15 @@ export class AnthropicLlm
|
|
|
297
297
|
return source.type === "base64" ? { ...source, media_type: mimeType } : source;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Bytes beat an address when a host sent both: it already paid for them on the way in, and the address it also
|
|
302
|
+
* sent is the one it renders — which the default storage backend serves on a path only the app can resolve.
|
|
303
|
+
* Picking that costs a confident answer about a picture nothing fetched; picking the bytes costs one hop that
|
|
304
|
+
* already carries them. A URL the provider really can reach travels alone.
|
|
305
|
+
*/
|
|
300
306
|
static sourceOf(attachment: AgentWireAttachment): AnthropicSource | null {
|
|
301
|
-
if (attachment.url) return { type: "url", url: attachment.url };
|
|
302
307
|
if (attachment.data) return { type: "base64", media_type: attachment.mimeType, data: attachment.data };
|
|
308
|
+
if (attachment.url) return { type: "url", url: attachment.url };
|
|
303
309
|
return null;
|
|
304
310
|
}
|
|
305
311
|
|
|
@@ -122,7 +122,7 @@ export class OpenaiDialect {
|
|
|
122
122
|
* Text attachments are labelled into the message, because a model handed two unlabelled documents can no longer
|
|
123
123
|
* cite either one. Images become their own parts only when the provider said it reads them; the dialect carries
|
|
124
124
|
* one as a `data:` URL, which is the same encoding whether the bytes were inlined or already addressable, so
|
|
125
|
-
* both carriers take one branch.
|
|
125
|
+
* both carriers take one branch — the inlined bytes first, for the reason named at the branch.
|
|
126
126
|
*/
|
|
127
127
|
static userContent(message: AgentWireMessage, accepts?: LlmAccepts): string | OpenaiContentPart[] {
|
|
128
128
|
const attachments = message.attachments ?? [];
|
|
@@ -144,7 +144,8 @@ export class OpenaiDialect {
|
|
|
144
144
|
);
|
|
145
145
|
return [];
|
|
146
146
|
}
|
|
147
|
-
|
|
147
|
+
|
|
148
|
+
const url = attachment.data ? `data:${mimeType};base64,${attachment.data}` : (attachment.url ?? "");
|
|
148
149
|
return url ? [{ type: "image_url" as const, image_url: { url } }] : [];
|
|
149
150
|
});
|
|
150
151
|
const text = [message.text, ...blocks, ...notes].filter(Boolean).join("\n\n");
|
package/signal/middleware.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { BackendEnv, Cls, PromiseOrObject } from "akanjs/base";
|
|
|
2
2
|
import { Logger } from "akanjs/common";
|
|
3
3
|
import { type CacheAdaptor, CacheAdaptorRole } from "akanjs/service";
|
|
4
4
|
import dayjs from "dayjs";
|
|
5
|
+
import { Exception } from "./exception";
|
|
5
6
|
import type { SignalContext } from "./signalContext";
|
|
6
7
|
import { traceCache } from "./trace";
|
|
7
8
|
|
|
@@ -46,71 +47,105 @@ export class Logging extends middleware("logging") {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Serves an endpoint's own answer back for as long as its declared `cache` allows, and stands aside for every
|
|
52
|
+
* endpoint that declared none — this is registered by default, so a TTL of its own would put a stale window on
|
|
53
|
+
* every call in the app.
|
|
54
|
+
*
|
|
55
|
+
* A cached answer is **shared**, which is why only a `query` taking no internal argument can carry one: internal
|
|
56
|
+
* arguments are how a call learns who is asking (`.with(Self)`), so an endpoint that has them answers per caller
|
|
57
|
+
* and one entry would be one caller's answer handed to the next. Guards are re-run on every hit regardless —
|
|
58
|
+
* shared is not public, and `next()`, which is what runs them, is exactly what a hit skips.
|
|
59
|
+
*/
|
|
49
60
|
export class Cache extends middleware("cache") {
|
|
61
|
+
static #topic = "cache";
|
|
62
|
+
static #refused = new Set<string>();
|
|
63
|
+
|
|
50
64
|
override async use() {
|
|
51
65
|
return async (context: SignalContext, next: () => Promise<unknown>) => {
|
|
66
|
+
const ttl = context.endpointInfo.signalOption.cache;
|
|
67
|
+
if (!ttl || !Number.isFinite(ttl) || ttl <= 0) return await next();
|
|
68
|
+
if (!Cache.#cacheable(context)) return await next();
|
|
52
69
|
const cache = context.getAdaptor(CacheAdaptorRole) as unknown as CacheAdaptor;
|
|
53
|
-
const topic = "cache";
|
|
54
70
|
const key = `${context.key}:${JSON.stringify(context.args)}`;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const parsed = JSON.parse(cached);
|
|
61
|
-
traceCache(true);
|
|
62
|
-
return parsed;
|
|
63
|
-
} catch (parseError) {
|
|
64
|
-
context.adaptor.logger.warn(`Cache parse error ${context.key}: ${String(parseError)}`);
|
|
65
|
-
await cache.delete(topic, key);
|
|
66
|
-
}
|
|
71
|
+
const cached = await Cache.#read(context, cache, key);
|
|
72
|
+
if (cached !== undefined) {
|
|
73
|
+
await context.checkGuards();
|
|
74
|
+
traceCache(true);
|
|
75
|
+
return cached;
|
|
67
76
|
}
|
|
68
77
|
traceCache(false);
|
|
69
|
-
|
|
70
78
|
const result = await next();
|
|
71
|
-
|
|
72
|
-
context.adaptor.logger.debug(`Caching result type ${context.key}: ${typeof result} / ${Array.isArray(result)}`);
|
|
73
|
-
|
|
74
|
-
const serialized = JSON.stringify(result);
|
|
75
|
-
await cache.set(topic, key, serialized, { expireAt: dayjs().add(60, "second") });
|
|
76
|
-
|
|
79
|
+
await Cache.#write(context, cache, key, result, ttl);
|
|
77
80
|
return result;
|
|
78
81
|
};
|
|
79
82
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
/**
|
|
84
|
+
* The handler's inputs are its declared arguments and its internal arguments, so an endpoint with none of the
|
|
85
|
+
* latter answers the same thing to everyone who may read it — which is the only answer a shared entry can hold.
|
|
86
|
+
* A `cache` declared anywhere else is named once rather than silently ignored.
|
|
87
|
+
*/
|
|
88
|
+
static #cacheable(context: SignalContext) {
|
|
89
|
+
if (context.endpointInfo.type === "query" && context.endpointInfo.internalArgs.length === 0) return true;
|
|
90
|
+
if (Cache.#refused.has(context.key)) return false;
|
|
91
|
+
Cache.#refused.add(context.key);
|
|
92
|
+
const reason =
|
|
93
|
+
context.endpointInfo.type === "query"
|
|
94
|
+
? "it takes an internal argument, so its answer is the caller's and not a shared one"
|
|
95
|
+
: `a ${context.endpointInfo.type} is never cached`;
|
|
96
|
+
context.adaptor.logger.warn(`"${context.key}" declares \`cache\` and cannot take one: ${reason}.`);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
/** A cache backend that is down must not take the endpoint down with it: the call runs uncached instead. */
|
|
100
|
+
static async #read(context: SignalContext, cache: CacheAdaptor, key: string) {
|
|
101
|
+
try {
|
|
102
|
+
const cached = await cache.get<string>(Cache.#topic, key);
|
|
103
|
+
if (cached == null) return undefined;
|
|
104
|
+
return JSON.parse(cached) as unknown;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
context.adaptor.logger.warn(`Cache read failed for ${context.key}: ${String(error)}`);
|
|
107
|
+
|
|
108
|
+
await cache.delete(Cache.#topic, key).catch(() => undefined);
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
static async #write(context: SignalContext, cache: CacheAdaptor, key: string, result: unknown, ttl: number) {
|
|
113
|
+
const serialized = JSON.stringify(result);
|
|
114
|
+
|
|
115
|
+
if (serialized === undefined) return;
|
|
116
|
+
try {
|
|
117
|
+
await cache.set(Cache.#topic, key, serialized, { expireAt: dayjs().add(ttl, "millisecond") });
|
|
118
|
+
} catch (error) {
|
|
119
|
+
context.adaptor.logger.warn(`Cache write failed for ${context.key}: ${String(error)}`);
|
|
120
|
+
}
|
|
91
121
|
}
|
|
92
122
|
}
|
|
93
123
|
|
|
94
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Bounds an endpoint that declared a `timeout`, and nothing else — this is registered by default, so a default
|
|
126
|
+
* of its own would put a deadline on every endpoint in the app that nobody asked for.
|
|
127
|
+
*
|
|
128
|
+
* XXX losing the race does not cancel the work: `next()` keeps running with nobody holding its result, so a
|
|
129
|
+
* handler that writes is still going to write. The deadline answers the caller; it does not undo the call.
|
|
130
|
+
*/
|
|
131
|
+
export class Timeout extends middleware("timeout") {
|
|
95
132
|
override async use() {
|
|
96
133
|
return async (context: SignalContext, next: () => Promise<unknown>) => {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
} catch (error) {
|
|
104
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
105
|
-
console.warn(`[${context.key}] Retry ${attempt + 1}/${maxRetries}:`, lastError.message);
|
|
134
|
+
const timeout = context.endpointInfo.signalOption.timeout;
|
|
135
|
+
if (!timeout || !Number.isFinite(timeout) || timeout <= 0) return await next();
|
|
136
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
137
|
+
try {
|
|
138
|
+
return await Promise.race([
|
|
139
|
+
next(),
|
|
106
140
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
141
|
+
new Promise((_, reject) => {
|
|
142
|
+
timer = setTimeout(() => reject(new Exception(504, "base.error.gatewayTimeout")), timeout);
|
|
143
|
+
}),
|
|
144
|
+
]);
|
|
145
|
+
} finally {
|
|
146
|
+
|
|
147
|
+
clearTimeout(timer);
|
|
112
148
|
}
|
|
113
|
-
throw lastError;
|
|
114
149
|
};
|
|
115
150
|
}
|
|
116
151
|
}
|
|
@@ -70,6 +70,7 @@ export class FetchSerializer {
|
|
|
70
70
|
...(endpointInfo.signalOption.path ? { path: endpointInfo.signalOption.path } : {}),
|
|
71
71
|
...(endpointInfo.signalOption.method ? { method: endpointInfo.signalOption.method } : {}),
|
|
72
72
|
...(endpointInfo.signalOption.fileUpload ? { fileUpload: true } : {}),
|
|
73
|
+
...(endpointInfo.signalOption.timeout ? { timeout: endpointInfo.signalOption.timeout } : {}),
|
|
73
74
|
...(guards?.length ? { guards } : {}),
|
|
74
75
|
...(endpointInfo.signalOption.mcp === false ? { mcp: false as const } : {}),
|
|
75
76
|
...(refusesAgents(endpointInfo.signalOption.guards) ? { agents: false as const } : {}),
|
package/signal/signalContext.ts
CHANGED
|
@@ -154,10 +154,17 @@ export class SignalContext<
|
|
|
154
154
|
throw new Exception.Forbidden(`Access denied by guard: ${GuardCls.name}`);
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* The endpoint's guards, for a middleware that answers a call without executing it — a cache hit skips
|
|
159
|
+
* `next()`, and `next()` is what would otherwise run them. Side-effect free, like the guards themselves.
|
|
160
|
+
*/
|
|
161
|
+
async checkGuards() {
|
|
162
|
+
await this.#checkGuards();
|
|
163
|
+
}
|
|
157
164
|
/**
|
|
158
165
|
* Re-checks this context's guards outside of a request, for a websocket room that is already
|
|
159
166
|
* subscribed. Only global middlewares run: they carry the account resolution this depends on,
|
|
160
|
-
* while endpoint middlewares (cache/
|
|
167
|
+
* while endpoint middlewares (cache/retry) would observe a call that never executes.
|
|
161
168
|
*/
|
|
162
169
|
async authorize(): Promise<boolean> {
|
|
163
170
|
try {
|
package/signal/types.ts
CHANGED
|
@@ -81,8 +81,26 @@ export interface SignalOption<Response = any, Nullable extends boolean = false,
|
|
|
81
81
|
default?: boolean;
|
|
82
82
|
path?: string;
|
|
83
83
|
serverMode?: "federation" | "batch" | "all";
|
|
84
|
+
/**
|
|
85
|
+
* How long this endpoint may take, in milliseconds. It bounds both ends: the `Timeout` middleware — registered
|
|
86
|
+
* by default — rejects the call with `base.error.gatewayTimeout` once it is spent, and the value is serialized
|
|
87
|
+
* to the client, where it becomes that call's request budget in place of the client default. Declared nowhere,
|
|
88
|
+
* neither side imposes one beyond the client's own default.
|
|
89
|
+
*
|
|
90
|
+
* Losing the race does not stop the work: the handler runs to completion with nobody holding its result.
|
|
91
|
+
*/
|
|
84
92
|
timeout?: number;
|
|
85
93
|
partial?: _Key[] | readonly _Key[];
|
|
94
|
+
/**
|
|
95
|
+
* How long this endpoint's answer may be reused, in milliseconds. The `Cache` middleware — registered by
|
|
96
|
+
* default — keeps the handler's result under the endpoint's key and its arguments and serves it until it
|
|
97
|
+
* expires; declared nowhere, nothing is cached.
|
|
98
|
+
*
|
|
99
|
+
* **Only a `query` that takes no internal argument may carry one.** Internal arguments are how a call learns
|
|
100
|
+
* who is asking (`.with(Self)`), so an endpoint that has them answers per caller, and one shared entry would be
|
|
101
|
+
* one caller's answer handed to the next; such an endpoint is named in the log and left uncached. Guards still
|
|
102
|
+
* run on every hit — a shared answer is not a public one.
|
|
103
|
+
*/
|
|
86
104
|
cache?: number;
|
|
87
105
|
guards?: GuardCls[];
|
|
88
106
|
middlewares?: MiddlewareCls[];
|
|
@@ -180,6 +198,12 @@ export interface SerializedArg {
|
|
|
180
198
|
export interface SerializedEndpoint extends SerializedSignalOption {
|
|
181
199
|
type: "query" | "mutation" | "pubsub" | "message";
|
|
182
200
|
returns: SerializedReturns;
|
|
201
|
+
/**
|
|
202
|
+
* The deadline the endpoint declared, in milliseconds. It travels because the client has to size its own
|
|
203
|
+
* request budget from it: a call the server is allowed to spend five minutes on is one the browser must not
|
|
204
|
+
* abandon after the client default.
|
|
205
|
+
*/
|
|
206
|
+
timeout?: number;
|
|
183
207
|
}
|
|
184
208
|
export interface SerializedFilter {
|
|
185
209
|
/** Every filter query the model declares, by key, with the args each one takes. */
|
package/types/common/types.d.ts
CHANGED
|
@@ -5,7 +5,11 @@ export interface FetchPolicy<Returns = unknown> {
|
|
|
5
5
|
onError?: (error: string) => void;
|
|
6
6
|
token?: string;
|
|
7
7
|
partial?: string[];
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Milliseconds before this call is abandoned, `false` to wait as long as the runtime will. Overrides the
|
|
10
|
+
* endpoint's declared `timeout`, which overrides the client's own default.
|
|
11
|
+
*/
|
|
12
|
+
timeout?: number | false;
|
|
9
13
|
/**
|
|
10
14
|
* A `pubsub` subscription only: called after the room has been resubscribed following a dropped connection.
|
|
11
15
|
*
|
|
@@ -55,6 +55,11 @@ export declare class FetchClient {
|
|
|
55
55
|
};
|
|
56
56
|
static resetSharedRegistry(): void;
|
|
57
57
|
static resetSharedClient(): void;
|
|
58
|
+
/**
|
|
59
|
+
* The budget for every call that neither names one nor is served by an endpoint declaring one. `false` waits
|
|
60
|
+
* as long as the runtime will, which is the browser's own limit — minutes.
|
|
61
|
+
*/
|
|
62
|
+
setTimeout(timeout?: number | false): void;
|
|
58
63
|
setErrorConstructor(ErrorCls?: ErrorConstructor): void;
|
|
59
64
|
applySignal(serializedSignal: {
|
|
60
65
|
[key: string]: SerializedSignal;
|
|
@@ -88,11 +93,13 @@ export declare class FetchClient {
|
|
|
88
93
|
fetch: any;
|
|
89
94
|
}>(constant: object, serializedSignal: {
|
|
90
95
|
[key: string]: SerializedSignal;
|
|
91
|
-
}, { origin, connect, base, Err, }?: {
|
|
96
|
+
}, { origin, connect, base, Err, timeout, }?: {
|
|
92
97
|
origin?: string;
|
|
93
98
|
connect?: boolean;
|
|
94
99
|
base?: FetchProxy;
|
|
95
100
|
Err?: ErrorConstructor;
|
|
101
|
+
/** This app's own default request budget, for calls no endpoint and no caller gave one. */
|
|
102
|
+
timeout?: number | false;
|
|
96
103
|
}): {
|
|
97
104
|
sig: ClientSignalMap<SigType>;
|
|
98
105
|
fetch: SigType["fetch"];
|
|
@@ -15,6 +15,8 @@ export declare class HttpClient {
|
|
|
15
15
|
private ErrorCls?;
|
|
16
16
|
constructor(baseUrl: string, options?: HttpClientOptions);
|
|
17
17
|
setErrorConstructor(ErrorCls?: ErrorConstructor): void;
|
|
18
|
+
/** The budget every call that names none takes. `false` waits as long as the runtime will. */
|
|
19
|
+
setTimeout(timeout?: number | false): void;
|
|
18
20
|
get<Returns = unknown>(url: string, options?: FetchOptions): Promise<Returns>;
|
|
19
21
|
send<Returns = unknown>(method: HttpMutationMethod, url: string, data: FormData | Record<string, unknown>, options?: FetchOptions): Promise<Returns>;
|
|
20
22
|
put<Returns = unknown>(url: string, data: FormData | Record<string, unknown>, options?: FetchOptions): Promise<Returns>;
|
|
@@ -109,6 +109,12 @@ export declare class AnthropicLlm extends AnthropicLlm_base implements LlmAdapto
|
|
|
109
109
|
static userContent(message: AgentWireMessage, accepts?: LlmAccepts): AnthropicBlock[];
|
|
110
110
|
/** The block's `media_type` is the essence, not whatever parameters the browser attached to it. */
|
|
111
111
|
static typed(source: AnthropicSource, mimeType: string): AnthropicSource;
|
|
112
|
+
/**
|
|
113
|
+
* Bytes beat an address when a host sent both: it already paid for them on the way in, and the address it also
|
|
114
|
+
* sent is the one it renders — which the default storage backend serves on a path only the app can resolve.
|
|
115
|
+
* Picking that costs a confident answer about a picture nothing fetched; picking the bytes costs one hop that
|
|
116
|
+
* already carries them. A URL the provider really can reach travels alone.
|
|
117
|
+
*/
|
|
112
118
|
static sourceOf(attachment: AgentWireAttachment): AnthropicSource | null;
|
|
113
119
|
static turnAnswer(answer: AnthropicAnswer): LlmTurnAnswer;
|
|
114
120
|
/** The ceiling wins over the calls that did arrive — see `OpenaiDialect.stopOf` for why. */
|
|
@@ -77,7 +77,7 @@ export declare class OpenaiDialect {
|
|
|
77
77
|
* Text attachments are labelled into the message, because a model handed two unlabelled documents can no longer
|
|
78
78
|
* cite either one. Images become their own parts only when the provider said it reads them; the dialect carries
|
|
79
79
|
* one as a `data:` URL, which is the same encoding whether the bytes were inlined or already addressable, so
|
|
80
|
-
* both carriers take one branch.
|
|
80
|
+
* both carriers take one branch — the inlined bytes first, for the reason named at the branch.
|
|
81
81
|
*/
|
|
82
82
|
static userContent(message: AgentWireMessage, accepts?: LlmAccepts): string | OpenaiContentPart[];
|
|
83
83
|
/**
|
|
@@ -27,8 +27,19 @@ declare const Cache_base: {
|
|
|
27
27
|
};
|
|
28
28
|
refName: string;
|
|
29
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* Serves an endpoint's own answer back for as long as its declared `cache` allows, and stands aside for every
|
|
32
|
+
* endpoint that declared none — this is registered by default, so a TTL of its own would put a stale window on
|
|
33
|
+
* every call in the app.
|
|
34
|
+
*
|
|
35
|
+
* A cached answer is **shared**, which is why only a `query` taking no internal argument can carry one: internal
|
|
36
|
+
* arguments are how a call learns who is asking (`.with(Self)`), so an endpoint that has them answers per caller
|
|
37
|
+
* and one entry would be one caller's answer handed to the next. Guards are re-run on every hit regardless —
|
|
38
|
+
* shared is not public, and `next()`, which is what runs them, is exactly what a hit skips.
|
|
39
|
+
*/
|
|
30
40
|
export declare class Cache extends Cache_base {
|
|
31
|
-
|
|
41
|
+
#private;
|
|
42
|
+
use(): Promise<(context: SignalContext, next: () => Promise<unknown>) => Promise<unknown>>;
|
|
32
43
|
}
|
|
33
44
|
declare const Timeout_base: {
|
|
34
45
|
new (): {
|
|
@@ -36,16 +47,14 @@ declare const Timeout_base: {
|
|
|
36
47
|
};
|
|
37
48
|
refName: string;
|
|
38
49
|
};
|
|
50
|
+
/**
|
|
51
|
+
* Bounds an endpoint that declared a `timeout`, and nothing else — this is registered by default, so a default
|
|
52
|
+
* of its own would put a deadline on every endpoint in the app that nobody asked for.
|
|
53
|
+
*
|
|
54
|
+
* XXX losing the race does not cancel the work: `next()` keeps running with nobody holding its result, so a
|
|
55
|
+
* handler that writes is still going to write. The deadline answers the caller; it does not undo the call.
|
|
56
|
+
*/
|
|
39
57
|
export declare class Timeout extends Timeout_base {
|
|
40
58
|
use(): Promise<(context: SignalContext, next: () => Promise<unknown>) => Promise<unknown>>;
|
|
41
59
|
}
|
|
42
|
-
declare const Retry_base: {
|
|
43
|
-
new (): {
|
|
44
|
-
use(env: BackendEnv): Promise<(context: SignalContext, next: () => Promise<unknown>) => Promise<unknown>>;
|
|
45
|
-
};
|
|
46
|
-
refName: string;
|
|
47
|
-
};
|
|
48
|
-
export declare class Retry extends Retry_base {
|
|
49
|
-
use(): Promise<(context: SignalContext, next: () => Promise<unknown>) => Promise<unknown>>;
|
|
50
|
-
}
|
|
51
60
|
export {};
|
|
@@ -56,10 +56,15 @@ export declare class SignalContext<Ctx extends HttpExecutionContext | WebSocketE
|
|
|
56
56
|
getAdaptor<T extends Adaptor>(adaptorCls: AdaptorCls<T>): T;
|
|
57
57
|
getService<T>(refName: string): T;
|
|
58
58
|
init(): Promise<this>;
|
|
59
|
+
/**
|
|
60
|
+
* The endpoint's guards, for a middleware that answers a call without executing it — a cache hit skips
|
|
61
|
+
* `next()`, and `next()` is what would otherwise run them. Side-effect free, like the guards themselves.
|
|
62
|
+
*/
|
|
63
|
+
checkGuards(): Promise<void>;
|
|
59
64
|
/**
|
|
60
65
|
* Re-checks this context's guards outside of a request, for a websocket room that is already
|
|
61
66
|
* subscribed. Only global middlewares run: they carry the account resolution this depends on,
|
|
62
|
-
* while endpoint middlewares (cache/
|
|
67
|
+
* while endpoint middlewares (cache/retry) would observe a call that never executes.
|
|
63
68
|
*/
|
|
64
69
|
authorize(): Promise<boolean>;
|
|
65
70
|
/**
|
package/types/signal/types.d.ts
CHANGED
|
@@ -69,8 +69,26 @@ export interface SignalOption<Response = any, Nullable extends boolean = false,
|
|
|
69
69
|
default?: boolean;
|
|
70
70
|
path?: string;
|
|
71
71
|
serverMode?: "federation" | "batch" | "all";
|
|
72
|
+
/**
|
|
73
|
+
* How long this endpoint may take, in milliseconds. It bounds both ends: the `Timeout` middleware — registered
|
|
74
|
+
* by default — rejects the call with `base.error.gatewayTimeout` once it is spent, and the value is serialized
|
|
75
|
+
* to the client, where it becomes that call's request budget in place of the client default. Declared nowhere,
|
|
76
|
+
* neither side imposes one beyond the client's own default.
|
|
77
|
+
*
|
|
78
|
+
* Losing the race does not stop the work: the handler runs to completion with nobody holding its result.
|
|
79
|
+
*/
|
|
72
80
|
timeout?: number;
|
|
73
81
|
partial?: _Key[] | readonly _Key[];
|
|
82
|
+
/**
|
|
83
|
+
* How long this endpoint's answer may be reused, in milliseconds. The `Cache` middleware — registered by
|
|
84
|
+
* default — keeps the handler's result under the endpoint's key and its arguments and serves it until it
|
|
85
|
+
* expires; declared nowhere, nothing is cached.
|
|
86
|
+
*
|
|
87
|
+
* **Only a `query` that takes no internal argument may carry one.** Internal arguments are how a call learns
|
|
88
|
+
* who is asking (`.with(Self)`), so an endpoint that has them answers per caller, and one shared entry would be
|
|
89
|
+
* one caller's answer handed to the next; such an endpoint is named in the log and left uncached. Guards still
|
|
90
|
+
* run on every hit — a shared answer is not a public one.
|
|
91
|
+
*/
|
|
74
92
|
cache?: number;
|
|
75
93
|
guards?: GuardCls[];
|
|
76
94
|
middlewares?: MiddlewareCls[];
|
|
@@ -168,6 +186,12 @@ export interface SerializedArg {
|
|
|
168
186
|
export interface SerializedEndpoint extends SerializedSignalOption {
|
|
169
187
|
type: "query" | "mutation" | "pubsub" | "message";
|
|
170
188
|
returns: SerializedReturns;
|
|
189
|
+
/**
|
|
190
|
+
* The deadline the endpoint declared, in milliseconds. It travels because the client has to size its own
|
|
191
|
+
* request budget from it: a call the server is allowed to spend five minutes on is one the browser must not
|
|
192
|
+
* abandon after the client default.
|
|
193
|
+
*/
|
|
194
|
+
timeout?: number;
|
|
171
195
|
}
|
|
172
196
|
export interface SerializedFilter {
|
|
173
197
|
/** Every filter query the model declares, by key, with the args each one takes. */
|
|
@@ -5,7 +5,7 @@ interface AttachProps {
|
|
|
5
5
|
onPick: (files: File[]) => void;
|
|
6
6
|
}
|
|
7
7
|
export declare const Attach: ({ className, label, onPick }: AttachProps) => import("react/jsx-runtime").JSX.Element;
|
|
8
|
-
interface ChipsProps {
|
|
8
|
+
export interface ChipsProps {
|
|
9
9
|
className?: string;
|
|
10
10
|
attachments: readonly MessageAttachment[];
|
|
11
11
|
/** Omitted for a sent message: what is already on the wire cannot be taken back. */
|
package/types/ui/Agent/Chat.d.ts
CHANGED
|
@@ -72,9 +72,10 @@ export interface ChatProps {
|
|
|
72
72
|
* the built-in, so it can also replace how an image is prepared.
|
|
73
73
|
*
|
|
74
74
|
* **A `url` is handed to the provider as the address it will fetch**, so answer `data` whenever the provider
|
|
75
|
-
* cannot reach it
|
|
76
|
-
*
|
|
77
|
-
*
|
|
75
|
+
* cannot reach it — the default storage backend serves a path only this app can resolve, and a model handed one
|
|
76
|
+
* answers about a picture it never saw with nothing anywhere reporting a failure. Answering **both** is the
|
|
77
|
+
* shape for that case: bytes are what the provider is given, and the address is what the chip draws, so an
|
|
78
|
+
* uploading reader gets a thumbnail without betting the answer on who can reach its storage.
|
|
78
79
|
*/
|
|
79
80
|
attach?: AttachReader;
|
|
80
81
|
/**
|
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
export interface SpinProps {
|
|
3
|
+
className?: string;
|
|
4
|
+
/**
|
|
5
|
+
* Replaces the built-in icon. It carries its own color; the rotation is the wrapper's
|
|
6
|
+
* (`[&>svg]:animate-spin`), so the node needs no `animate-spin` of its own.
|
|
7
|
+
*/
|
|
3
8
|
indicator?: ReactNode;
|
|
4
9
|
isCenter?: boolean;
|
|
5
|
-
|
|
6
|
-
size?: "sm" | "md" | "lg";
|
|
10
|
+
/** A named step, or the pixel size the icon is drawn at. */
|
|
11
|
+
size?: "sm" | "md" | "lg" | number;
|
|
12
|
+
/**
|
|
13
|
+
* What the built-in icon is colored with. `"current"` inherits the surface's own foreground, which is what a
|
|
14
|
+
* filled surface needs — `text-primary/70` is legible on the app background and vanishes on a `bg-info` badge
|
|
15
|
+
* or a primary button. Every tone loses to a `text-*` in `className`.
|
|
16
|
+
*/
|
|
17
|
+
tone?: "primary" | "current" | "muted";
|
|
7
18
|
}
|
|
8
|
-
|
|
19
|
+
/**
|
|
20
|
+
* The color and the size sit on the wrapper, not on the icon: the icon is drawn at `1em` in `currentColor`, so
|
|
21
|
+
* both cascade to it — and `className`, which is also the wrapper's, is merged last and therefore wins. On the
|
|
22
|
+
* icon they would have been unreachable, which is what made every `text-*` and `size-*` a caller passed a no-op.
|
|
23
|
+
*/
|
|
24
|
+
export declare const Spin: ({ className, indicator, isCenter, size, tone }: SpinProps) => import("react/jsx-runtime").JSX.Element;
|
package/types/ui/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { AgentProvider, type AgentProviderProps, type AgentRunner, AgentSession, type AgentSessionOptions, type ChatMessage, type CompactOptions, type ContextBlock, httpRunner, type MessageAttachment, type PublishedTool, type RunnerEvent, type RunnerRequest, SessionContext, type SessionHistory, type SurfaceView, useAgent, } from "../vendor/use-agentic.d.ts";
|
|
2
2
|
export { Agent } from "./Agent.d.ts";
|
|
3
3
|
export { type ApprovalProps, DefaultApproval } from "./Agent/Approval.d.ts";
|
|
4
|
+
export { Chips as AgentAttachments, type ChipsProps as AgentAttachmentsProps } from "./Agent/Attach.d.ts";
|
|
4
5
|
export { type AgentSessionSetup, agentSessionOf } from "./Agent/agentSessionOf.d.ts";
|
|
5
6
|
export { type AttachLimits, type AttachReader, maxAttachmentBytes, maxMessageAttachmentBytes, maxMessageAttachments, } from "./Agent/attachment.d.ts";
|
|
6
7
|
export { type BubbleProps, DefaultBubble } from "./Agent/Bubble.d.ts";
|
package/ui/Agent/Attach.tsx
CHANGED
|
@@ -38,7 +38,7 @@ export const Attach = ({ className, label, onPick }: AttachProps) => {
|
|
|
38
38
|
);
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
interface ChipsProps {
|
|
41
|
+
export interface ChipsProps {
|
|
42
42
|
className?: string;
|
|
43
43
|
|
|
44
44
|
attachments: readonly MessageAttachment[];
|
|
@@ -57,11 +57,11 @@ export const Chips = ({ className, attachments, onRemove, removeLabel, pending =
|
|
|
57
57
|
className="flex items-center gap-1 rounded-field bg-muted px-2 py-0.5 text-xs"
|
|
58
58
|
key={`${attachment.name}-${idx}`}
|
|
59
59
|
>
|
|
60
|
-
{attachment.data && attachment.mimeType?.startsWith("image/") ? (
|
|
60
|
+
{(attachment.data || attachment.url) && attachment.mimeType?.startsWith("image/") ? (
|
|
61
61
|
<img
|
|
62
62
|
alt={attachment.name}
|
|
63
63
|
className="size-6 rounded-field object-cover"
|
|
64
|
-
src={`data:${attachment.mimeType};base64,${attachment.data}`}
|
|
64
|
+
src={attachment.data ? `data:${attachment.mimeType};base64,${attachment.data}` : attachment.url}
|
|
65
65
|
/>
|
|
66
66
|
) : null}
|
|
67
67
|
<span className="max-w-32 truncate">{attachment.name}</span>
|
package/ui/Agent/Chat.tsx
CHANGED
|
@@ -111,9 +111,10 @@ export interface ChatProps {
|
|
|
111
111
|
* the built-in, so it can also replace how an image is prepared.
|
|
112
112
|
*
|
|
113
113
|
* **A `url` is handed to the provider as the address it will fetch**, so answer `data` whenever the provider
|
|
114
|
-
* cannot reach it
|
|
115
|
-
*
|
|
116
|
-
*
|
|
114
|
+
* cannot reach it — the default storage backend serves a path only this app can resolve, and a model handed one
|
|
115
|
+
* answers about a picture it never saw with nothing anywhere reporting a failure. Answering **both** is the
|
|
116
|
+
* shape for that case: bytes are what the provider is given, and the address is what the chip draws, so an
|
|
117
|
+
* uploading reader gets a thumbnail without betting the answer on who can reach its storage.
|
|
117
118
|
*/
|
|
118
119
|
attach?: AttachReader;
|
|
119
120
|
/**
|
|
@@ -8,16 +8,19 @@ export type PersistOption = boolean | { storage?: "session" | "local"; key?: str
|
|
|
8
8
|
* chunk of it, and `AgentSession` swallows a failed save — so persisting the bytes would quietly stop persisting
|
|
9
9
|
* the transcript itself. The name and type stay so a restored conversation still reads as what happened, and a
|
|
10
10
|
* `url` stays because a pointer is not content; the server then tells the model the content is gone rather than
|
|
11
|
-
* letting it answer from the filename.
|
|
11
|
+
* letting it answer from the filename. A `ref` stays for the same reason and matters more: it is the host's own
|
|
12
|
+
* handle on the file, so dropping it leaves a restored conversation with nothing but a name and size to guess
|
|
13
|
+
* from — the guess `ref` exists to retire.
|
|
12
14
|
*/
|
|
13
15
|
const withoutContent = (message: ChatMessage): ChatMessage =>
|
|
14
16
|
message.attachments?.length
|
|
15
17
|
? {
|
|
16
18
|
...message,
|
|
17
|
-
attachments: message.attachments.map(({ name, mimeType, url }) => ({
|
|
19
|
+
attachments: message.attachments.map(({ name, mimeType, url, ref }) => ({
|
|
18
20
|
name,
|
|
19
21
|
mimeType,
|
|
20
22
|
...(url ? { url } : {}),
|
|
23
|
+
...(ref ? { ref } : {}),
|
|
21
24
|
})),
|
|
22
25
|
}
|
|
23
26
|
: message;
|
package/ui/Loading/Spin.tsx
CHANGED
|
@@ -3,26 +3,46 @@ import type { ReactNode } from "react";
|
|
|
3
3
|
import { AiOutlineLoading3Quarters } from "react-icons/ai";
|
|
4
4
|
|
|
5
5
|
export interface SpinProps {
|
|
6
|
+
className?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Replaces the built-in icon. It carries its own color; the rotation is the wrapper's
|
|
9
|
+
* (`[&>svg]:animate-spin`), so the node needs no `animate-spin` of its own.
|
|
10
|
+
*/
|
|
6
11
|
indicator?: ReactNode;
|
|
7
12
|
isCenter?: boolean;
|
|
8
|
-
|
|
9
|
-
size?: "sm" | "md" | "lg";
|
|
13
|
+
/** A named step, or the pixel size the icon is drawn at. */
|
|
14
|
+
size?: "sm" | "md" | "lg" | number;
|
|
15
|
+
/**
|
|
16
|
+
* What the built-in icon is colored with. `"current"` inherits the surface's own foreground, which is what a
|
|
17
|
+
* filled surface needs — `text-primary/70` is legible on the app background and vanishes on a `bg-info` badge
|
|
18
|
+
* or a primary button. Every tone loses to a `text-*` in `className`.
|
|
19
|
+
*/
|
|
20
|
+
tone?: "primary" | "current" | "muted";
|
|
10
21
|
}
|
|
11
22
|
|
|
12
23
|
const sizeClass = { sm: "text-sm", md: "text-xl", lg: "text-3xl" } as const;
|
|
24
|
+
const toneClass = { primary: "text-primary/70", current: "", muted: "text-muted-foreground" } as const;
|
|
13
25
|
|
|
14
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The color and the size sit on the wrapper, not on the icon: the icon is drawn at `1em` in `currentColor`, so
|
|
28
|
+
* both cascade to it — and `className`, which is also the wrapper's, is merged last and therefore wins. On the
|
|
29
|
+
* icon they would have been unreachable, which is what made every `text-*` and `size-*` a caller passed a no-op.
|
|
30
|
+
*/
|
|
31
|
+
export const Spin = ({ className, indicator, isCenter, size = "md", tone = "primary" }: SpinProps) => (
|
|
15
32
|
<div
|
|
16
33
|
className={cn(
|
|
17
34
|
"inline-block py-1",
|
|
35
|
+
!indicator && toneClass[tone],
|
|
36
|
+
typeof size === "string" && sizeClass[size],
|
|
18
37
|
isCenter && "absolute inset-0 flex size-full items-center justify-center py-0",
|
|
19
38
|
className,
|
|
20
39
|
)}
|
|
40
|
+
style={typeof size === "number" ? { fontSize: size } : undefined}
|
|
21
41
|
>
|
|
22
42
|
{indicator ? (
|
|
23
43
|
<span className="[&>svg]:animate-spin">{indicator}</span>
|
|
24
44
|
) : (
|
|
25
|
-
<AiOutlineLoading3Quarters className=
|
|
45
|
+
<AiOutlineLoading3Quarters className="animate-spin" />
|
|
26
46
|
)}
|
|
27
47
|
</div>
|
|
28
48
|
);
|
package/ui/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export {
|
|
|
21
21
|
} from "../vendor/use-agentic";
|
|
22
22
|
export { Agent } from "./Agent";
|
|
23
23
|
export { type ApprovalProps, DefaultApproval } from "./Agent/Approval";
|
|
24
|
+
export { Chips as AgentAttachments, type ChipsProps as AgentAttachmentsProps } from "./Agent/Attach";
|
|
24
25
|
export { type AgentSessionSetup, agentSessionOf } from "./Agent/agentSessionOf";
|
|
25
26
|
export {
|
|
26
27
|
type AttachLimits,
|
|
@@ -100,7 +100,9 @@ export class Compaction {
|
|
|
100
100
|
static #line(message: ChatMessage): string {
|
|
101
101
|
const parts: string[] = [];
|
|
102
102
|
if (message.text) parts.push(Compaction.#clip(message.text, 1200));
|
|
103
|
-
|
|
103
|
+
|
|
104
|
+
for (const attachment of message.attachments ?? [])
|
|
105
|
+
parts.push(`[attached ${attachment.name}, content not carried into this summary]`);
|
|
104
106
|
for (const call of message.toolCalls ?? [])
|
|
105
107
|
parts.push(`[called ${call.name} ${Compaction.#clip(JSON.stringify(call.args), 200)}]`);
|
|
106
108
|
for (const result of message.toolResults ?? [])
|