akanjs 3.0.0-beta.0 → 3.0.0-beta.10
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/index.ts +1 -0
- package/common/pathGet.ts +12 -2
- package/common/pathSet.ts +2 -3
- package/common/toPathSegments.ts +10 -0
- package/common/types.ts +5 -1
- package/dictionary/agent.dictionary.ts +8 -0
- package/dictionary/agentTurn.dictionary.ts +6 -2
- package/dictionary/base.dictionary.ts +6 -0
- package/fetch/agentTurn.ts +1 -1
- package/fetch/client/fetchClient.ts +20 -2
- package/fetch/client/httpClient.ts +4 -0
- package/fetch/client/wsClient.ts +4 -4
- package/index.ts +5 -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/agent.service.ts +99 -4
- package/service/predefinedAdaptor/anthropicLlm.ts +409 -0
- package/service/predefinedAdaptor/deepseekLlm.ts +21 -211
- package/service/predefinedAdaptor/index.ts +3 -0
- package/service/predefinedAdaptor/llm.adaptor.ts +42 -1
- package/service/predefinedAdaptor/openaiDialect.ts +267 -0
- package/service/predefinedAdaptor/openaiLlm.ts +91 -0
- package/signal/agentTurnStream.ts +5 -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/store/agentic/index.ts +1 -0
- package/store/agentic/useAgentReference.ts +49 -0
- package/store/hooks.ts +1 -1
- package/types/common/index.d.ts +1 -0
- package/types/common/toPathSegments.d.ts +9 -0
- package/types/common/types.d.ts +5 -1
- package/types/dictionary/agent.dictionary.d.ts +1 -1
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +9 -9
- package/types/fetch/agentTurn.d.ts +3 -3
- package/types/fetch/client/fetchClient.d.ts +8 -1
- package/types/fetch/client/httpClient.d.ts +2 -0
- package/types/index.d.ts +5 -0
- package/types/service/agent.service.d.ts +55 -1
- package/types/service/predefinedAdaptor/anthropicLlm.d.ts +131 -0
- package/types/service/predefinedAdaptor/deepseekLlm.d.ts +10 -67
- package/types/service/predefinedAdaptor/index.d.ts +3 -0
- package/types/service/predefinedAdaptor/llm.adaptor.d.ts +41 -1
- package/types/service/predefinedAdaptor/openaiDialect.d.ts +103 -0
- package/types/service/predefinedAdaptor/openaiLlm.d.ts +24 -0
- package/types/signal/agent.signal.d.ts +1 -1
- package/types/signal/agentTurn.d.ts +1 -1
- package/types/signal/agentTurnStream.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/store/agentic/index.d.ts +1 -0
- package/types/store/agentic/useAgentReference.d.ts +32 -0
- package/types/store/hooks.d.ts +1 -1
- package/types/ui/Agent/Attach.d.ts +5 -2
- package/types/ui/Agent/Chat.d.ts +17 -1
- package/types/ui/Agent/Composer.d.ts +11 -2
- package/types/ui/Agent/Menu.d.ts +4 -2
- package/types/ui/Agent/Refer.d.ts +13 -0
- package/types/ui/Agent/Steps.d.ts +33 -0
- package/types/ui/Agent/useChatAttachments.d.ts +1 -0
- package/types/ui/Agent/useChatQueue.d.ts +3 -1
- package/types/ui/Agent/useChatReferences.d.ts +26 -0
- package/types/ui/Agent/useReferenceMenu.d.ts +43 -0
- package/types/ui/Field/lightRefCache.d.ts +19 -0
- package/types/ui/Field/useRelationOptions.d.ts +39 -0
- package/types/ui/Loading/Spin.d.ts +19 -3
- package/types/ui/Select.d.ts +2 -0
- package/types/ui/UiOverride/context.d.ts +2 -0
- package/types/ui/index.d.ts +6 -1
- package/types/vendor/use-agentic/AgentSession.d.ts +54 -1
- package/types/vendor/use-agentic/Reference.d.ts +56 -0
- package/types/vendor/use-agentic/Transcript.d.ts +2 -1
- package/types/vendor/use-agentic/index.d.ts +1 -0
- package/types/vendor/use-agentic/types.d.ts +37 -1
- package/ui/Agent/Attach.tsx +17 -4
- package/ui/Agent/Bubble.tsx +2 -0
- package/ui/Agent/Chat.tsx +73 -13
- package/ui/Agent/Composer.tsx +29 -3
- package/ui/Agent/Menu.tsx +8 -3
- package/ui/Agent/Queued.tsx +2 -0
- package/ui/Agent/Refer.tsx +44 -0
- package/ui/Agent/Steps.tsx +49 -0
- package/ui/Agent/sessionHistory.ts +38 -12
- package/ui/Agent/useChatAttachments.ts +6 -0
- package/ui/Agent/useChatQueue.ts +11 -1
- package/ui/Agent/useChatReferences.ts +67 -0
- package/ui/Agent/useReferenceMenu.ts +108 -0
- package/ui/Field/Relation.tsx +70 -150
- package/ui/Field/lightRefCache.ts +73 -0
- package/ui/Field/useRelationOptions.ts +106 -0
- package/ui/Loading/Spin.tsx +24 -4
- package/ui/Select.tsx +24 -14
- package/ui/UiOverride/context.ts +2 -0
- package/ui/index.ts +11 -0
- package/vendor/use-agentic/AgentSession.ts +142 -3
- package/vendor/use-agentic/Compaction.ts +9 -1
- package/vendor/use-agentic/Reference.ts +99 -0
- package/vendor/use-agentic/Transcript.ts +9 -3
- package/vendor/use-agentic/WIRE.md +6 -1
- package/vendor/use-agentic/httpRunner.ts +1 -1
- package/vendor/use-agentic/index.ts +1 -0
- package/vendor/use-agentic/types.ts +39 -1
package/common/index.ts
CHANGED
|
@@ -111,6 +111,7 @@ export { sleep } from "./sleep";
|
|
|
111
111
|
export { splitVersion } from "./splitVersion";
|
|
112
112
|
export { getBasePathFromPathname, parseBasePaths, parseSubRouteHosts, resolveSubRouteHosts } from "./subRoute";
|
|
113
113
|
export { TrustedProxy } from "./TrustedProxy";
|
|
114
|
+
export { toPathSegments } from "./toPathSegments";
|
|
114
115
|
export type * from "./types";
|
|
115
116
|
export {
|
|
116
117
|
type WebsocketAuthAckData,
|
package/common/pathGet.ts
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
|
+
import { toPathSegments } from "./toPathSegments";
|
|
2
|
+
|
|
1
3
|
type Indexable = Record<string | number, unknown>;
|
|
4
|
+
type PathSegment = string | number;
|
|
2
5
|
|
|
3
6
|
const isIndexable = (value: unknown): value is Indexable => Object(value) === value;
|
|
4
7
|
|
|
8
|
+
const readChild = (container: Indexable, key: PathSegment) =>
|
|
9
|
+
container instanceof Map ? (container as Map<PathSegment, unknown>).get(key) : container[key];
|
|
10
|
+
|
|
5
11
|
export const pathGet = (
|
|
6
12
|
path: string | (string | number)[],
|
|
7
13
|
obj: unknown,
|
|
8
14
|
separator = ".",
|
|
9
15
|
fallback: unknown = null,
|
|
10
16
|
): unknown => {
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
|
|
18
|
+
const properties = separator === "." ? toPathSegments(path) : Array.isArray(path) ? [...path] : path.split(separator);
|
|
19
|
+
return properties.reduce<unknown>(
|
|
20
|
+
(prev, curr) => (isIndexable(prev) ? (readChild(prev, curr) ?? fallback) : fallback),
|
|
21
|
+
obj,
|
|
22
|
+
);
|
|
13
23
|
};
|
package/common/pathSet.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
+
import { toPathSegments } from "./toPathSegments";
|
|
2
|
+
|
|
1
3
|
type MutableIndexable = Record<string | number, unknown>;
|
|
2
4
|
type PathSegment = string | number;
|
|
3
5
|
type Container = MutableIndexable | Map<PathSegment, unknown>;
|
|
4
6
|
|
|
5
|
-
const toPathSegments = (path: string | readonly PathSegment[]) =>
|
|
6
|
-
Array.isArray(path) ? [...path] : path.toString().match(/[^.[\]]+/g) || [];
|
|
7
|
-
|
|
8
7
|
const readChild = (container: Container, key: PathSegment) =>
|
|
9
8
|
container instanceof Map ? container.get(key) : container[key];
|
|
10
9
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type PathSegment = string | number;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The one definition of what a dotted path's segments are, so a path that writes and a path that reads cannot
|
|
5
|
+
* disagree about it. `a.0.b` and `a[0].b` are the same three segments — the bracket form is what a form field
|
|
6
|
+
* hands `writeOn<Model>`, and a read of the same path has to accept the same spelling or the agent can write
|
|
7
|
+
* somewhere it cannot read back.
|
|
8
|
+
*/
|
|
9
|
+
export const toPathSegments = (path: string | readonly PathSegment[]) =>
|
|
10
|
+
Array.isArray(path) ? [...path] : path.toString().match(/[^.[\]]+/g) || [];
|
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
|
*
|
|
@@ -28,4 +28,12 @@ export const agentDictionary = serviceDictionary(["en", "ko"])
|
|
|
28
28
|
"DeepSeek refused this turn with status {status}. Reason: {reason}",
|
|
29
29
|
"DeepSeek가 이번 턴을 거절했습니다 (status {status}). 사유: {reason}",
|
|
30
30
|
],
|
|
31
|
+
openaiRequestFailed: [
|
|
32
|
+
"OpenAI refused this turn with status {status}. Reason: {reason}",
|
|
33
|
+
"OpenAI가 이번 턴을 거절했습니다 (status {status}). 사유: {reason}",
|
|
34
|
+
],
|
|
35
|
+
anthropicRequestFailed: [
|
|
36
|
+
"Anthropic refused this turn with status {status}. Reason: {reason}",
|
|
37
|
+
"Anthropic이 이번 턴을 거절했습니다 (status {status}). 사유: {reason}",
|
|
38
|
+
],
|
|
31
39
|
});
|
|
@@ -19,11 +19,15 @@ export const agentTurnDictionary = scalarDictionary(["en", "ko"])
|
|
|
19
19
|
"클라이언트가 실행할 툴 호출 목록 ({ id, name, args })",
|
|
20
20
|
]),
|
|
21
21
|
stop: t(["Stop", "종료 사유"]).desc([
|
|
22
|
-
"Why the turn ended — end,
|
|
23
|
-
"턴이 끝난 이유 — end
|
|
22
|
+
"Why the turn ended — end, toolUse when tool results are awaited, or length when the provider cut it off",
|
|
23
|
+
"턴이 끝난 이유 — end, 툴 결과를 기다리는 toolUse, 프로바이더가 잘라낸 length",
|
|
24
24
|
]),
|
|
25
25
|
}))
|
|
26
26
|
.enum<AgentStop>("agentStop", (t) => ({
|
|
27
27
|
end: t(["End", "종료"]).desc(["The final answer", "최종 응답"]),
|
|
28
28
|
toolUse: t(["Tool Use", "툴 사용"]).desc(["The model awaits tool results", "모델이 툴 결과를 기다린다"]),
|
|
29
|
+
length: t(["Length", "길이 초과"]).desc([
|
|
30
|
+
"The provider's answer ceiling cut the turn off, so it is incomplete",
|
|
31
|
+
"프로바이더의 응답 상한에 걸려 턴이 잘렸다. 미완성이다",
|
|
32
|
+
]),
|
|
29
33
|
}));
|
|
@@ -80,6 +80,12 @@ export const baseDictionary = serviceDictionary(["en", "ko"])
|
|
|
80
80
|
agentVoiceFailed: ["The microphone could not be used.", "마이크를 사용할 수 없습니다."],
|
|
81
81
|
agentAttach: ["Attach a file", "파일 첨부"],
|
|
82
82
|
agentAttachRemove: ["Remove attachment", "첨부 제거"],
|
|
83
|
+
agentAttachReading: ["Reading…", "읽는 중…"],
|
|
84
|
+
agentReferenceRemove: ["Remove reference", "참조 제거"],
|
|
85
|
+
agentReferenceFailed: [
|
|
86
|
+
"{label} could not be read, so the agent has only its name.",
|
|
87
|
+
"{label}을(를) 읽지 못해 이름만 전달됩니다.",
|
|
88
|
+
],
|
|
83
89
|
agentAttachTooLarge: ["{name} is too large to attach.", "{name}은(는) 용량이 너무 커서 첨부할 수 없습니다."],
|
|
84
90
|
agentAttachUnsupported: ["{name} cannot be attached here.", "{name}은(는) 여기에 첨부할 수 없습니다."],
|
|
85
91
|
agentAttachDuplicate: ["{name} is already attached.", "{name}은(는) 이미 첨부되어 있습니다."],
|
package/fetch/agentTurn.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Any, enumOf } from "akanjs/base";
|
|
2
2
|
import { ConstantRegistry, via } from "akanjs/constant";
|
|
3
3
|
|
|
4
|
-
export class AgentStop extends enumOf("agentStop", ["end", "toolUse"] as const) {}
|
|
4
|
+
export class AgentStop extends enumOf("agentStop", ["end", "toolUse", "length"] as const) {}
|
|
5
5
|
|
|
6
6
|
export class AgentTurn extends via((field) => ({
|
|
7
7
|
text: field(String, { default: "" }), // the assistant's words; empty when the turn is only tool calls
|
|
@@ -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
|
}
|
package/fetch/client/wsClient.ts
CHANGED
|
@@ -171,13 +171,13 @@ export class WsClient {
|
|
|
171
171
|
break;
|
|
172
172
|
}
|
|
173
173
|
} catch (error) {
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
175
|
+
this.logger.warn(`WebSocket message process failed ${errMsg}`);
|
|
176
176
|
}
|
|
177
177
|
};
|
|
178
178
|
this.#ws.onerror = (e) => {
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
180
|
+
this.logger.verbose(`WebSocket error ${errMsg}`);
|
|
181
181
|
};
|
|
182
182
|
this.#ws.onclose = (event) => {
|
|
183
183
|
this.logger.debug(`WebSocket closed: ${event.code} ${event.reason}`);
|
package/index.ts
CHANGED
|
@@ -292,6 +292,11 @@ export interface SubspaceDeclaration {
|
|
|
292
292
|
repo: string;
|
|
293
293
|
/** Apps this subspace serves. Libraries are never listed — they are derived from each app's closure. */
|
|
294
294
|
apps: string[];
|
|
295
|
+
/**
|
|
296
|
+
* The cloud workspace this subspace deploys from — its own `AKAN_WORKSPACE_ID`, not this workspace's.
|
|
297
|
+
* `akan subspace upload-env` is the only thing that reads it.
|
|
298
|
+
*/
|
|
299
|
+
workspaceId?: string;
|
|
295
300
|
}
|
|
296
301
|
|
|
297
302
|
/**
|
|
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);
|
package/service/agent.service.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Err } from "akanjs/dictionary";
|
|
|
2
2
|
import type {
|
|
3
3
|
AgentWireAttachment,
|
|
4
4
|
AgentWireMessage,
|
|
5
|
+
AgentWireReference,
|
|
5
6
|
LlmAccepts,
|
|
6
7
|
LlmTurnRequest,
|
|
7
8
|
} from "./predefinedAdaptor/llm.adaptor";
|
|
@@ -16,7 +17,9 @@ export class AgentService extends serve("agent" as const, ({ plug }) => ({
|
|
|
16
17
|
|
|
17
18
|
const names = ToolNames.of(request);
|
|
18
19
|
const prepared = names.encode(
|
|
19
|
-
AgentService.instructed(
|
|
20
|
+
AgentService.instructed(
|
|
21
|
+
AgentService.readable(AgentService.referenced(AgentService.explained(request)), this.llm.accepts),
|
|
22
|
+
),
|
|
20
23
|
);
|
|
21
24
|
const answer = await this.llm.chat(prepared, onDelta);
|
|
22
25
|
if (!answer) throw new Err("agent.error.llmUnavailable");
|
|
@@ -68,6 +71,94 @@ export class AgentService extends serve("agent" as const, ({ plug }) => ({
|
|
|
68
71
|
return { ...rest, text: [message.text, `[The turn failed: ${error}]`].filter(Boolean).join("\n\n") };
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
/**
|
|
75
|
+
* The ceiling one reference's value may add to a turn, mirroring the client's own — the browser clips before it
|
|
76
|
+
* stages, and this is the same answer given again where nothing can route around it. `runTurn` is the only path
|
|
77
|
+
* to `chat()`, so a host that builds the wire itself, an older client, and a replayed transcript all pass here.
|
|
78
|
+
*/
|
|
79
|
+
static readonly referenceLimit = 20_000;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Folds what the user pointed at into the message they pointed with, as text.
|
|
83
|
+
*
|
|
84
|
+
* Text rather than a carrier of its own for the same reason the note in `readable` is text: it is the one field
|
|
85
|
+
* every provider mapping already reads, so Anthropic, the OpenAI dialect and DeepSeek need no change between
|
|
86
|
+
* them and none of them can drop a reference quietly.
|
|
87
|
+
*
|
|
88
|
+
* The heading rides once per message rather than once per reference, and lives here rather than in `preamble`
|
|
89
|
+
* because most conversations never carry one — a sentence about mention tokens in every turn of every app is
|
|
90
|
+
* paid by every app that has no references at all.
|
|
91
|
+
*/
|
|
92
|
+
static referenced(request: LlmTurnRequest): LlmTurnRequest {
|
|
93
|
+
if (!request.messages.some((message) => message.references?.length)) return request;
|
|
94
|
+
return { ...request, messages: request.messages.map((message) => AgentService.referencedMessage(message)) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private static referencedMessage(message: AgentWireMessage): AgentWireMessage {
|
|
98
|
+
const { references = [], ...rest } = message;
|
|
99
|
+
if (!references.length) return message;
|
|
100
|
+
const block = [AgentService.referenceHeading, ...references.map(AgentService.referenceLine)].join("\n\n");
|
|
101
|
+
return { ...rest, text: [message.text, block].filter(Boolean).join("\n\n") };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Not decoration, and not free to shorten. Both halves of the sentence were observed doing their job, against
|
|
106
|
+
* Anthropic, with the same referenced field and only the published tools changed:
|
|
107
|
+
*
|
|
108
|
+
* - With no tool on the screen to re-read with, the model opened its answer by saying so — that it could see
|
|
109
|
+
* only the snapshot and the field might have been edited since. Unprompted, ahead of the answer.
|
|
110
|
+
* - With the screen's own tools passed (a state read and a write), it issued the read instead of answering,
|
|
111
|
+
* and stopped the turn there.
|
|
112
|
+
*
|
|
113
|
+
* So it reads as an instruction rather than as framing: it re-reads where it can and says it cannot where it
|
|
114
|
+
* cannot, which is the pair a turn confidently quoting a stale value is bought against. Two scenarios against
|
|
115
|
+
* one provider, not the per-cell runs behind `preamble` — enough to keep the sentence, not enough to call it
|
|
116
|
+
* measured.
|
|
117
|
+
*/
|
|
118
|
+
static readonly referenceHeading =
|
|
119
|
+
"[Referenced data: the user pointed at this while writing the message above, with the @[label](mention:…) " +
|
|
120
|
+
"tokens in it. Each value is what it was at the moment they sent the message, not what it is now — read it " +
|
|
121
|
+
"again with a tool before relying on it, and do not assume an edit you have made since is reflected here.]";
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A string value is printed as itself rather than as JSON. It is the common case — one field of one document —
|
|
125
|
+
* and a quoted, escaped copy of a paragraph is harder for a model to read back and to quote from than the
|
|
126
|
+
* paragraph. It is also what a clipped value already is, so the cut JSON prints as the fragment it is.
|
|
127
|
+
*
|
|
128
|
+
* The pointer leads the line rather than riding the wire alone, and that is what the label cannot do. Pointed
|
|
129
|
+
* at a saved document while a *different* document of the same model sat open on the screen, the model
|
|
130
|
+
* compared the two ids, said the edit it could make would land on the wrong one, and stopped to ask — with no
|
|
131
|
+
* write call. Two references sharing a label are still two ids here, so keep the id ahead of the label.
|
|
132
|
+
*/
|
|
133
|
+
private static referenceLine(reference: AgentWireReference): string {
|
|
134
|
+
const at = `${reference.refName}/${reference.refId}${reference.path ? `#${reference.path}` : ""}`;
|
|
135
|
+
const head = `${at} (${reference.label}):`;
|
|
136
|
+
if (reference.value === undefined)
|
|
137
|
+
return `${head} [not read: ${reference.note ?? "the value was not carried into this conversation"}]`;
|
|
138
|
+
const text =
|
|
139
|
+
typeof reference.value === "string" ? reference.value : (JSON.stringify(reference.value, null, 2) ?? "null");
|
|
140
|
+
const body =
|
|
141
|
+
text.length <= AgentService.referenceLimit
|
|
142
|
+
? text
|
|
143
|
+
: `${text.slice(0, AgentService.referenceLimit)}…\n[Clipped at ${AgentService.referenceLimit} characters.]`;
|
|
144
|
+
return `${head}\n${AgentService.fenced(body)}${reference.note ? `\n[${reference.note}]` : ""}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Where a value ends. A multi-line one — the usual shape of the prose field somebody points at — otherwise runs
|
|
149
|
+
* straight into the next reference's heading, and the model reads one value that swallowed the next label.
|
|
150
|
+
*
|
|
151
|
+
* The fence grows past the longest backtick run inside the value, which is CommonMark's own answer to the same
|
|
152
|
+
* problem: a fixed fence is one that a value containing a fence breaks out of, and a value containing a fence is
|
|
153
|
+
* ordinary here, because the thing being pointed at is often something a person wrote.
|
|
154
|
+
*/
|
|
155
|
+
private static fenced(text: string): string {
|
|
156
|
+
const runs = text.match(/`+/g);
|
|
157
|
+
const longest = runs ? Math.max(...runs.map((run) => run.length)) : 0;
|
|
158
|
+
const fence = "`".repeat(Math.max(3, longest + 1));
|
|
159
|
+
return `${fence}\n${text}\n${fence}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
71
162
|
/**
|
|
72
163
|
* Replaces every attachment the provider cannot read with a note naming it, so no adaptor has to think about
|
|
73
164
|
* attachments it does not support and none can lose one quietly. The model has to be *told*, not merely spared:
|
|
@@ -98,14 +189,18 @@ export class AgentService extends serve("agent" as const, ({ plug }) => ({
|
|
|
98
189
|
private static isReadable(attachment: AgentWireAttachment, accepts: LlmAccepts): boolean {
|
|
99
190
|
if (attachment.text) return true;
|
|
100
191
|
if (!attachment.data && !attachment.url) return false;
|
|
192
|
+
|
|
193
|
+
if (typeof attachment.mimeType !== "string") return false;
|
|
101
194
|
return attachment.mimeType.startsWith("image/") ? !!accepts.image : !!accepts.document;
|
|
102
195
|
}
|
|
103
196
|
|
|
104
197
|
private static note(attachment: AgentWireAttachment): string {
|
|
105
198
|
const why =
|
|
106
|
-
attachment.data
|
|
107
|
-
? "
|
|
108
|
-
:
|
|
199
|
+
!attachment.data && !attachment.url
|
|
200
|
+
? "its content is no longer available, as a reloaded conversation keeps the name and not the bytes"
|
|
201
|
+
: typeof attachment.mimeType === "string"
|
|
202
|
+
? "this model cannot read that type"
|
|
203
|
+
: "it names no type it could be read as";
|
|
109
204
|
return `[Attachment not read: ${attachment.name} (${attachment.mimeType}) — ${why}. Tell the user it was not read instead of guessing what it holds, and ask for the text if the answer needs it.]`;
|
|
110
205
|
}
|
|
111
206
|
}
|