@desplega.ai/agent-swarm 2.4.4 → 2.4.5
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/dist/{acp-adapter-thq04adh.js → acp-adapter-2wdkktv6.js} +1 -1
- package/dist/{actions-j6ywh6x5.js → actions-c40ebcm4.js} +18 -18
- package/dist/{amp-adapter-k8b5xmj8.js → amp-adapter-wb67m95z.js} +2 -2
- package/dist/{app-rb7vc02s.js → app-353vre8g.js} +3 -3
- package/dist/{assistant-ftta1zjf.js → assistant-sj1njh23.js} +21 -21
- package/dist/{boot-reembed-srp6v6af.js → boot-reembed-b9c3az3c.js} +1 -1
- package/dist/{boot-reembed-xns7h5h9.js → boot-reembed-qdkvxeem.js} +1 -1
- package/dist/{boot-scrub-sweep-3qqrh2mz.js → boot-scrub-sweep-m3wh8eks.js} +2 -2
- package/dist/{boot-seeds-9ks8hce7.js → boot-seeds-j2273s54.js} +4 -4
- package/dist/{claude-adapter-k5ksyres.js → claude-adapter-m7q0a085.js} +2 -2
- package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-5ptb6tzs.js} +2 -2
- package/dist/{claude-managed-setup-n5a5eqks.js → claude-managed-setup-smrqk5j5.js} +3 -3
- package/dist/{claude-sdk-session-2j3j4kwn.js → claude-sdk-session-38xnz21e.js} +8284 -8267
- package/dist/{cli-cynvc9fj.js → cli-018r4jq6.js} +5 -5
- package/dist/{cli-9fhkkpj5.js → cli-1f3qzw6y.js} +1 -1
- package/dist/{cli-dqb4n629.js → cli-1sp861s4.js} +7 -7
- package/dist/{cli-gqvh8d8a.js → cli-1syn2xy1.js} +10 -10
- package/dist/{cli-qkhc5aqy.js → cli-37j04k05.js} +11 -11
- package/dist/{cli-1d70crsy.js → cli-3a45zcgm.js} +1 -1
- package/dist/{cli-t7cyef5g.js → cli-43x64hv3.js} +4 -4
- package/dist/{cli-hj3debgw.js → cli-4zfqgjc5.js} +2 -2
- package/dist/{cli-55qmbp6z.js → cli-7c9qs530.js} +1 -1
- package/dist/{cli-9qtjfd60.js → cli-848hxdwj.js} +1 -1
- package/dist/{cli-pccfmtb5.js → cli-94gwyg7m.js} +7 -7
- package/dist/{cli-51y09c6p.js → cli-9hstj09h.js} +5 -5
- package/dist/{cli-zrwpctp4.js → cli-9yqgm9nd.js} +22 -22
- package/dist/{cli-esxerzg7.js → cli-a92r1127.js} +1 -1
- package/dist/{cli-b8k7epj1.js → cli-be2161h1.js} +7 -7
- package/dist/{cli-xc27fwzg.js → cli-bnes4z6b.js} +2 -2
- package/dist/{cli-zrk6jxv8.js → cli-cxxe61f6.js} +1 -1
- package/dist/{cli-amyhm3gj.js → cli-dk44dxec.js} +1 -1
- package/dist/{cli-nc35pxb7.js → cli-eww120g8.js} +1 -0
- package/dist/{cli-a0kcty51.js → cli-f01h25yt.js} +1 -1
- package/dist/{cli-82d3nkc3.js → cli-f0g3pgvf.js} +1 -1
- package/dist/{cli-1ephgwnq.js → cli-ftc1g5tj.js} +2 -2
- package/dist/{cli-2v045xyf.js → cli-fv5hepyy.js} +3 -3
- package/dist/{cli-bnz0r3kc.js → cli-g35m2xwm.js} +4 -4
- package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
- package/dist/{cli-w6nadkh4.js → cli-j99azg3g.js} +3 -3
- package/dist/{cli-er83cjbn.js → cli-k8g16gz3.js} +416 -116
- package/dist/{cli-4dg55wsk.js → cli-kpw04va0.js} +1 -1
- package/dist/{cli-3cgngyd8.js → cli-ms85309d.js} +33 -15
- package/dist/{cli-v3yr2gbg.js → cli-n72rg53p.js} +1471 -635
- package/dist/{cli-37yb7307.js → cli-p14m2cnp.js} +1 -1
- package/dist/{cli-2zqpkvtt.js → cli-qfxc84gh.js} +1 -1
- package/dist/{cli-44159k28.js → cli-qwhy2qma.js} +1 -1
- package/dist/{cli-2y91wnbh.js → cli-st8aw1m6.js} +221 -9
- package/dist/{cli-vq97g1jq.js → cli-tw4p90jb.js} +92 -192
- package/dist/{cli-te6b5wvg.js → cli-v0yrfmdh.js} +1 -1
- package/dist/{cli-xj0re5tm.js → cli-v8fjakwf.js} +1 -1
- package/dist/{cli-p20r8w42.js → cli-wt0jf9hm.js} +215 -80
- package/dist/{cli-q6r7t299.js → cli-xpa64rek.js} +33 -11
- package/dist/{cli-vdm20wej.js → cli-z7d29rw9.js} +2 -2
- package/dist/cli.js +19 -19
- package/dist/{codex-adapter-968xwfy7.js → codex-adapter-tn6k5mkg.js} +1 -1
- package/dist/{codex-session-runner-ht4z99fa.js → codex-session-runner-7vjt7jbw.js} +1 -1
- package/dist/{commands-pcymaeg9.js → commands-t7rz9146.js} +2 -2
- package/dist/{controller-fpjyk46r.js → controller-m2jasma5.js} +2 -2
- package/dist/{controllers-connect-mwzx1fh6.js → controllers-connect-9ftpex99.js} +19 -19
- package/dist/{cursor-adapter-cp4s2e2f.js → cursor-adapter-mjv79e4z.js} +1 -1
- package/dist/{db-xsjg9jqq.js → db-6sfrn9g3.js} +1 -1
- package/dist/{db-sgd12bt9.js → db-rn673r06.js} +3 -3
- package/dist/{dispatcher-x0jfw2c9.js → dispatcher-c155xz0p.js} +17 -17
- package/dist/{dsh-adapter-jrq7ma7h.js → dsh-adapter-3mmszc1x.js} +1 -1
- package/dist/{external-channel-8njj4h3b.js → external-channel-35k6tkv8.js} +2 -2
- package/dist/{grok-adapter-kkr97v2f.js → grok-adapter-tfrq7tg1.js} +2 -2
- package/dist/handlers-7gw64dxw.js +39 -0
- package/dist/{heartbeat-wd03zhn0.js → heartbeat-dyg4npkp.js} +20 -20
- package/dist/{hook-f0vghj42.js → hook-drspbwpe.js} +1 -1
- package/dist/{hook-xzj2ts2p.js → hook-x26b31bg.js} +1 -1
- package/dist/{http-4hwxv62p.js → http-pk972xyw.js} +355 -216
- package/dist/{index-ww2vznkp.js → index-arytgj5q.js} +1 -1
- package/dist/{index-hg0x2cdv.js → index-bgdyvcnv.js} +17 -17
- package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
- package/dist/{index-gdh8jrdm.js → index-gabbcdn2.js} +17 -17
- package/dist/{index-97q4xn03.js → index-jwxkkex2.js} +129 -75
- package/dist/index-tfb26zhy.js +41 -0
- package/dist/{keepalive-hrh7xnr9.js → keepalive-7yrng7px.js} +3 -3
- package/dist/{lead-ee8vazz2.js → lead-7e02z839.js} +29 -29
- package/dist/{maintenance-e8wnwnqx.js → maintenance-e6cemdqt.js} +1 -1
- package/dist/{node-3v3f08f7.js → node-de6akczr.js} +10 -9
- package/dist/{oauth-refresh-sweep-zm0yj4sj.js → oauth-refresh-sweep-yw3xdbcj.js} +2 -2
- package/dist/{onboard-dtwcq603.js → onboard-0a9y5yrc.js} +2 -2
- package/dist/{otel-impl-w3fbhpn0.js → otel-impl-trjpkfb7.js} +1 -1
- package/dist/{pricing-refresh-j38dehfs.js → pricing-refresh-d2b5my0y.js} +3 -3
- package/dist/{rbac-roles-ejxx9b35.js → rbac-roles-hey3yv9d.js} +2 -2
- package/dist/{rbac-roles-ae8yttq4.js → rbac-roles-nxh5ahwk.js} +1 -1
- package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
- package/dist/{render-v2-n16n0phe.js → render-v2-rr3p2atg.js} +3 -3
- package/dist/{resume-z0fgjc38.js → resume-2efkf1s1.js} +5 -5
- package/dist/{session-access-cv3mq6mc.js → session-access-s2mttxpw.js} +4 -4
- package/dist/{setup-mafhj3yt.js → setup-vypma5yt.js} +1 -1
- package/dist/{work-objects-8b86mgjp.js → work-objects-rvhckdkp.js} +20 -20
- package/dist/{worker-16s75hm5.js → worker-5snytpbd.js} +29 -29
- package/openapi.json +9 -2
- package/package.json +7 -7
- package/src/apps/catalog.generated.json +206 -4
- package/src/apps/page-validator.ts +13 -1
- package/src/be/chunking.ts +42 -9
- package/src/be/db/tasks/draft-text.ts +15 -0
- package/src/be/db/tasks/read.ts +14 -1
- package/src/be/db.ts +5 -1
- package/src/be/memory/index-content.ts +36 -17
- package/src/be/memory/providers/openai-embedding.ts +115 -61
- package/src/be/memory/providers/sqlite-store.ts +69 -23
- package/src/be/memory/reranker.ts +31 -1
- package/src/be/memory/search-candidates.ts +47 -0
- package/src/be/memory/types.ts +14 -0
- package/src/be/scripts/typecheck.ts +2 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +106 -54
- package/src/be/seed-scripts/index.ts +2 -2
- package/src/be/swarm-config-guard.ts +24 -1
- package/src/be/task-attachment-store.ts +30 -0
- package/src/extensions/catalog.generated.json +4 -4
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +7 -0
- package/src/fs/provider.ts +13 -1
- package/src/heartbeat/heartbeat.ts +31 -11
- package/src/http/fs.ts +54 -34
- package/src/http/mcp-body.ts +37 -0
- package/src/http/mcp-user.ts +3 -5
- package/src/http/mcp.ts +6 -7
- package/src/http/memory.ts +26 -15
- package/src/http/page-proxy.ts +61 -5
- package/src/http/pages-public.ts +27 -44
- package/src/http/pages.ts +4 -4
- package/src/http/script-runs.ts +2 -0
- package/src/http/users.ts +12 -1
- package/src/integrations/chat-sdk/adapters.ts +7 -3
- package/src/integrations/chat-sdk/attachments.ts +90 -12
- package/src/integrations/chat-sdk/facts.ts +120 -0
- package/src/integrations/chat-sdk/inbound.ts +156 -8
- package/src/integrations/chat-sdk/limits.ts +24 -0
- package/src/integrations/chat-sdk/outbound-files.ts +186 -0
- package/src/integrations/chat-sdk/outbound.ts +234 -4
- package/src/integrations/chat-sdk/relay.ts +4 -1
- package/src/integrations/chat-sdk/router.ts +28 -2
- package/src/integrations/chat-sdk/steering.ts +9 -0
- package/src/integrations/chat-sdk/templates.ts +14 -0
- package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
- package/src/mcp-limits.ts +20 -0
- package/src/pages/browser-sdk.ts +8 -3
- package/src/pages/feedback-overlay.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +106 -119
- package/src/providers/codex-agents-md.ts +35 -229
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/realtime/transport.ts +1 -2
- package/src/script-workflows/workflow-ctx.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +2 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +8 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
- package/src/server-user.ts +2 -0
- package/src/server.ts +2 -0
- package/src/slack/assistant.ts +2 -2
- package/src/slack/files.ts +2 -0
- package/src/slack/handlers.ts +4 -4
- package/src/slack/inbound-files.ts +39 -6
- package/src/slack/thread-buffer.ts +2 -1
- package/src/tasks/task-terminal-effects.ts +21 -17
- package/src/tests/apps-card-grid.test.ts +197 -0
- package/src/tests/apps-catalog-additions.test.ts +120 -0
- package/src/tests/apps-tree.test.ts +129 -0
- package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
- package/src/tests/chat-sdk-config.test.ts +11 -0
- package/src/tests/chat-sdk-facts.test.ts +23 -0
- package/src/tests/chat-sdk-inbound.test.ts +558 -4
- package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
- package/src/tests/codex-adapter.test.ts +167 -242
- package/src/tests/config-api-only-keys.test.ts +9 -0
- package/src/tests/cors-allowlist.test.ts +1 -1
- package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
- package/src/tests/heartbeat-checklist.test.ts +33 -0
- package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
- package/src/tests/heartbeat-jev-gate.test.ts +242 -1
- package/src/tests/kv-page-proxy.test.ts +91 -5
- package/src/tests/launch-password-rejection.test.ts +2 -2
- package/src/tests/mcp-input-caps.test.ts +294 -0
- package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
- package/src/tests/memory-embedding-input-cap.test.ts +114 -0
- package/src/tests/memory.test.ts +14 -0
- package/src/tests/page-guest-session.test.ts +19 -8
- package/src/tests/page-proxy-authed.test.ts +1 -1
- package/src/tests/page-proxy.test.ts +17 -10
- package/src/tests/page-session.test.ts +102 -1
- package/src/tests/pages-authed-mode.test.ts +82 -12
- package/src/tests/pages-password-mode.test.ts +5 -5
- package/src/tests/realtime-transport.test.ts +13 -7
- package/src/tests/script-runs-http.test.ts +84 -0
- package/src/tests/scripts-http.test.ts +10 -1
- package/src/tests/swarm-config-export-to-env.test.ts +22 -3
- package/src/tests/task-audio-transcription.test.ts +299 -0
- package/src/tests/task-completion-memory-chunking.test.ts +434 -0
- package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
- package/src/tests/transcription.test.ts +299 -0
- package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
- package/src/tests/workflow-system-one-decision.test.ts +2 -1
- package/src/tools/chat-access.ts +10 -0
- package/src/tools/chat-post.ts +31 -11
- package/src/tools/chat-reply.ts +32 -10
- package/src/tools/memory-search.ts +3 -1
- package/src/tools/system-one-decide.ts +17 -4
- package/src/types.ts +6 -0
- package/src/utils/config-env-export.ts +3 -1
- package/src/utils/page-session.ts +133 -20
- package/src/utils/transcription.ts +246 -0
- package/src/workflows/executors/system-one-decision.ts +28 -7
- package/src/workflows/executors/system-one-providers.ts +39 -4
- package/templates/extensions/slack-reply-gate/README.md +24 -22
- package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
- package/templates/extensions/slack-reply-gate/manifest.json +2 -2
- package/templates/skills/apps/SKILL.md +28 -6
- package/templates/skills/apps/content.md +28 -6
- package/templates/skills/chat-sdk/content.md +6 -2
- package/templates/skills/code-quality/content.md +2 -1
- package/templates/skills/heartbeat-runbook/content.md +5 -5
- package/templates/skills/pages/content.md +6 -4
- package/templates/skills/swarm-scripts/SKILL.md +2 -2
- package/templates/skills/swarm-scripts/content.md +2 -2
- package/dist/handlers-0pvee9nw.js +0 -39
- package/dist/index-thm0y6a8.js +0 -41
|
@@ -3,12 +3,40 @@ import { getOpenRouterAttributionHeaders } from "../../../utils/openrouter-base-
|
|
|
3
3
|
import { DEFAULT_EMBEDDING_MODEL, EMBEDDING_DIMENSIONS } from "../constants";
|
|
4
4
|
import type { EmbeddingProvider } from "../types";
|
|
5
5
|
|
|
6
|
+
// text-embedding-3-* reject any input over 8,192 tokens with a 400, and one
|
|
7
|
+
// such input fails every input in its request. There is no tokenizer here, and
|
|
8
|
+
// dense text (UUIDs, hashes, CJK) measures 1.2-1.8 chars per token, so no char
|
|
9
|
+
// cap is safe by itself: start at ~3 chars per token and halve on rejection.
|
|
10
|
+
export const MAX_EMBEDDING_INPUT_CHARS = 24_000;
|
|
11
|
+
const MIN_EMBEDDING_INPUT_CHARS = 1_000;
|
|
12
|
+
// The API also caps one request at 300,000 tokens. Even at one char per token,
|
|
13
|
+
// this keeps a request under it.
|
|
14
|
+
const MAX_BATCH_REQUEST_CHARS = 200_000;
|
|
15
|
+
|
|
6
16
|
interface OpenAIEmbeddingConfig {
|
|
7
17
|
apiKey?: string;
|
|
8
18
|
model?: string;
|
|
9
19
|
dimensions?: number;
|
|
10
20
|
}
|
|
11
21
|
|
|
22
|
+
function cleanInput(text: string): string {
|
|
23
|
+
return text
|
|
24
|
+
.replace(/[\n\r]/g, " ")
|
|
25
|
+
.trim()
|
|
26
|
+
.slice(0, MAX_EMBEDDING_INPUT_CHARS);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A 400 is about the request's inputs, so the other inputs can still embed. */
|
|
30
|
+
function isInputRejection(err: unknown): boolean {
|
|
31
|
+
return err instanceof OpenAI.APIError && err.status === 400;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isInputTooLong(err: unknown): boolean {
|
|
35
|
+
return (
|
|
36
|
+
isInputRejection(err) && /maximum (context|input) length/i.test((err as Error).message ?? "")
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
12
40
|
export class OpenAIEmbeddingProvider implements EmbeddingProvider {
|
|
13
41
|
readonly name: string;
|
|
14
42
|
readonly dimensions: number;
|
|
@@ -47,83 +75,109 @@ export class OpenAIEmbeddingProvider implements EmbeddingProvider {
|
|
|
47
75
|
return this.client;
|
|
48
76
|
}
|
|
49
77
|
|
|
50
|
-
async
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
input: cleaned,
|
|
61
|
-
dimensions: this.dimensions,
|
|
62
|
-
// Without it the SDK asks for base64 and decodes any reply as base64. A
|
|
63
|
-
// gateway that answers with plain floats would then yield garbage.
|
|
64
|
-
encoding_format: "float",
|
|
65
|
-
});
|
|
78
|
+
private async request(client: OpenAI, input: string | string[]) {
|
|
79
|
+
return client.embeddings.create({
|
|
80
|
+
model: this.model,
|
|
81
|
+
input,
|
|
82
|
+
dimensions: this.dimensions,
|
|
83
|
+
// Without it the SDK asks for base64 and decodes any reply as base64. A
|
|
84
|
+
// gateway that answers with plain floats would then yield garbage.
|
|
85
|
+
encoding_format: "float",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
66
88
|
|
|
67
|
-
|
|
68
|
-
|
|
89
|
+
private toVector(values: number[] | undefined, label: string): Float32Array | null {
|
|
90
|
+
if (!values) return null;
|
|
91
|
+
if (values.length !== this.dimensions) {
|
|
92
|
+
console.error(
|
|
93
|
+
`[memory] ${label} dimension mismatch: expected=${this.dimensions} got=${values.length}. Provider may not support the 'dimensions' parameter.`,
|
|
94
|
+
);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return new Float32Array(values);
|
|
98
|
+
}
|
|
69
99
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
100
|
+
/** Embed one cleaned input, cutting it in half each time the provider says it is too long. */
|
|
101
|
+
private async embedOne(client: OpenAI, cleaned: string): Promise<Float32Array | null> {
|
|
102
|
+
let input = cleaned;
|
|
103
|
+
while (true) {
|
|
104
|
+
try {
|
|
105
|
+
const response = await this.request(client, input);
|
|
106
|
+
return this.toVector(response.data[0]?.embedding, "Embedding");
|
|
107
|
+
} catch (err) {
|
|
108
|
+
const message = (err as Error).message;
|
|
109
|
+
if (isInputTooLong(err) && input.length > MIN_EMBEDDING_INPUT_CHARS) {
|
|
110
|
+
const next = Math.max(MIN_EMBEDDING_INPUT_CHARS, Math.floor(input.length / 2));
|
|
111
|
+
console.warn(
|
|
112
|
+
`[memory] Embedding input over the token cap at ${input.length} chars, retrying at ${next}: ${message}`,
|
|
113
|
+
);
|
|
114
|
+
input = input.slice(0, next);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
console.error("[memory] Embedding failed:", message);
|
|
74
118
|
return null;
|
|
75
119
|
}
|
|
76
|
-
|
|
77
|
-
return new Float32Array(values);
|
|
78
|
-
} catch (err) {
|
|
79
|
-
console.error("[memory] Embedding failed:", (err as Error).message);
|
|
80
|
-
return null;
|
|
81
120
|
}
|
|
82
121
|
}
|
|
83
122
|
|
|
84
|
-
async
|
|
123
|
+
async embed(text: string): Promise<Float32Array | null> {
|
|
85
124
|
const client = this.getClient();
|
|
86
|
-
if (!client) return
|
|
125
|
+
if (!client) return null;
|
|
87
126
|
|
|
88
|
-
const cleaned =
|
|
89
|
-
|
|
90
|
-
|
|
127
|
+
const cleaned = cleanInput(text);
|
|
128
|
+
if (!cleaned) return null;
|
|
129
|
+
return this.embedOne(client, cleaned);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async embedBatch(texts: string[]): Promise<(Float32Array | null)[]> {
|
|
133
|
+
const results: (Float32Array | null)[] = texts.map(() => null);
|
|
134
|
+
const client = this.getClient();
|
|
135
|
+
if (!client) return results;
|
|
91
136
|
|
|
137
|
+
const cleaned = texts.map(cleanInput);
|
|
138
|
+
const groups: number[][] = [];
|
|
139
|
+
let group: number[] = [];
|
|
140
|
+
let groupChars = 0;
|
|
92
141
|
for (let i = 0; i < cleaned.length; i++) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
142
|
+
const length = cleaned[i]!.length;
|
|
143
|
+
if (length === 0) continue;
|
|
144
|
+
if (group.length > 0 && groupChars + length > MAX_BATCH_REQUEST_CHARS) {
|
|
145
|
+
groups.push(group);
|
|
146
|
+
group = [];
|
|
147
|
+
groupChars = 0;
|
|
96
148
|
}
|
|
149
|
+
group.push(i);
|
|
150
|
+
groupChars += length;
|
|
97
151
|
}
|
|
152
|
+
if (group.length > 0) groups.push(group);
|
|
98
153
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
154
|
+
for (const indices of groups) {
|
|
155
|
+
try {
|
|
156
|
+
const response = await this.request(
|
|
157
|
+
client,
|
|
158
|
+
indices.map((i) => cleaned[i]!),
|
|
159
|
+
);
|
|
160
|
+
for (const item of response.data) {
|
|
161
|
+
const originalIndex = indices[item.index];
|
|
162
|
+
if (originalIndex === undefined) continue;
|
|
163
|
+
results[originalIndex] = this.toVector(item.embedding, "Batch embedding");
|
|
164
|
+
}
|
|
165
|
+
} catch (err) {
|
|
166
|
+
if (!isInputRejection(err)) {
|
|
167
|
+
console.error("[memory] Batch embedding failed:", (err as Error).message);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
// One rejected input fails the whole request: embed each input alone so
|
|
171
|
+
// only that input is cut down, and the rest embed as they are.
|
|
172
|
+
console.warn(
|
|
173
|
+
`[memory] Batch embedding rejected, embedding ${indices.length} inputs one by one: ${(err as Error).message}`,
|
|
174
|
+
);
|
|
175
|
+
for (const i of indices) {
|
|
176
|
+
results[i] = await this.embedOne(client, cleaned[i]!);
|
|
120
177
|
}
|
|
121
178
|
}
|
|
122
|
-
|
|
123
|
-
return results;
|
|
124
|
-
} catch (err) {
|
|
125
|
-
console.error("[memory] Batch embedding failed:", (err as Error).message);
|
|
126
|
-
return texts.map(() => null);
|
|
127
179
|
}
|
|
180
|
+
|
|
181
|
+
return results;
|
|
128
182
|
}
|
|
129
183
|
}
|
|
@@ -12,8 +12,10 @@ import {
|
|
|
12
12
|
} from "../constants";
|
|
13
13
|
import { isLongtermKey, LONGTERM_ROOT, tierSource } from "../key-paths";
|
|
14
14
|
import { recencyDecay } from "../reranker";
|
|
15
|
+
import { MAX_CANDIDATE_FETCH } from "../search-candidates";
|
|
15
16
|
import type {
|
|
16
17
|
MemoryCandidate,
|
|
18
|
+
MemoryDocumentRef,
|
|
17
19
|
MemoryEditInput,
|
|
18
20
|
MemoryEditResult,
|
|
19
21
|
MemoryHealth,
|
|
@@ -29,6 +31,16 @@ import type {
|
|
|
29
31
|
|
|
30
32
|
const VECTOR_BYTES = EMBEDDING_DIMENSIONS * Float32Array.BYTES_PER_ELEMENT;
|
|
31
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Rows one hybrid or FTS arm fetches for a `limit`-row search: 4x overfetch up
|
|
36
|
+
* to 100, never fewer than `limit`. A fixed 100 ceiling returned the same rows
|
|
37
|
+
* for every wider limit, so searchMemoryCandidates read it as exhaustion while
|
|
38
|
+
* other documents sat behind 100 chunks of one task.
|
|
39
|
+
*/
|
|
40
|
+
function armFetchLimit(limit: number): number {
|
|
41
|
+
return Math.min(Math.max(Math.min(limit * 4, 100), limit), MAX_CANDIDATE_FETCH);
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
// memory_fts.memory_id is UNINDEXED (FTS5 cannot index it), so a CORRELATED
|
|
33
45
|
// lookup against it is a full table scan per outer row. The old backfill used
|
|
34
46
|
// `WHERE NOT EXISTS (SELECT 1 FROM memory_fts f WHERE f.memory_id = m.id)`,
|
|
@@ -512,6 +524,32 @@ export class SqliteMemoryStore implements MemoryStore {
|
|
|
512
524
|
return results;
|
|
513
525
|
}
|
|
514
526
|
|
|
527
|
+
async replaceDocument(
|
|
528
|
+
document: MemoryDocumentRef,
|
|
529
|
+
inputs: MemoryInput[],
|
|
530
|
+
): Promise<AgentMemory[]> {
|
|
531
|
+
const results: AgentMemory[] = [];
|
|
532
|
+
await getDbClient().transaction(async (tx) => {
|
|
533
|
+
// Only this owner's rows of this source, in this scope, under this key.
|
|
534
|
+
// Another source holding the key is left alone and the insert fails.
|
|
535
|
+
const rows = await tx.query<{ id: string }>(
|
|
536
|
+
`SELECT id FROM agent_memory
|
|
537
|
+
WHERE key = ? AND scope = ? AND COALESCE(agentId, '') = COALESCE(?, '') AND source = ?`,
|
|
538
|
+
[document.key, document.scope, document.agentId ?? null, document.source],
|
|
539
|
+
);
|
|
540
|
+
const ids = rows.map((row) => row.id);
|
|
541
|
+
if (ids.length > 0) {
|
|
542
|
+
await this.purgeByIds(ids);
|
|
543
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
544
|
+
await tx.run(`DELETE FROM agent_memory WHERE id IN (${placeholders})`, ids);
|
|
545
|
+
}
|
|
546
|
+
for (const input of inputs) {
|
|
547
|
+
results.push(await this.store(input));
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
return results;
|
|
551
|
+
}
|
|
552
|
+
|
|
515
553
|
async get(id: string): Promise<AgentMemory | null> {
|
|
516
554
|
const db = getDbClient();
|
|
517
555
|
const row = await db.get<AgentMemoryRow>("SELECT * FROM agent_memory WHERE id = ?", [id]);
|
|
@@ -624,7 +662,7 @@ export class SqliteMemoryStore implements MemoryStore {
|
|
|
624
662
|
keyPrefix?: string;
|
|
625
663
|
},
|
|
626
664
|
): Promise<MemoryCandidate[]> {
|
|
627
|
-
const overfetchLimit =
|
|
665
|
+
const overfetchLimit = armFetchLimit(options.limit);
|
|
628
666
|
const vectorCandidates = await this.searchWithVec(queryEmbedding, agentId, {
|
|
629
667
|
...options,
|
|
630
668
|
limit: overfetchLimit,
|
|
@@ -725,7 +763,7 @@ export class SqliteMemoryStore implements MemoryStore {
|
|
|
725
763
|
}
|
|
726
764
|
|
|
727
765
|
try {
|
|
728
|
-
const sqlLimit =
|
|
766
|
+
const sqlLimit = armFetchLimit(limit);
|
|
729
767
|
const rows = await getDbClient().query<AgentMemoryRow & { rank: number }>(
|
|
730
768
|
`SELECT m.*, bm25(memory_fts) AS rank
|
|
731
769
|
FROM memory_fts
|
|
@@ -1254,29 +1292,37 @@ export class SqliteMemoryStore implements MemoryStore {
|
|
|
1254
1292
|
}
|
|
1255
1293
|
|
|
1256
1294
|
async updateEmbedding(id: string, embedding: Float32Array, model: string): Promise<void> {
|
|
1257
|
-
const db = getDbClient();
|
|
1258
1295
|
const buffer = serializeEmbedding(embedding);
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1296
|
+
// One transaction: a delete or replaceDocument queued behind this write
|
|
1297
|
+
// cannot land between the row UPDATE and the vec insert and orphan it.
|
|
1298
|
+
await getDbClient().transaction(async (tx) => {
|
|
1299
|
+
const updated = await tx.run(
|
|
1300
|
+
"UPDATE agent_memory SET embedding = ?, embeddingModel = ? WHERE id = ?",
|
|
1301
|
+
[buffer, model, id],
|
|
1302
|
+
);
|
|
1303
|
+
// The row was deleted (or replaced) while its embedding was in flight:
|
|
1304
|
+
// a vec row now would point at nothing.
|
|
1305
|
+
if (updated.changes === 0) return;
|
|
1306
|
+
|
|
1307
|
+
if (this.vecInitialized && this.getVecTableSchema()) {
|
|
1308
|
+
const vecBuffer = this.toVecBuffer(embedding);
|
|
1309
|
+
if (!vecBuffer) {
|
|
1310
|
+
console.warn(
|
|
1311
|
+
`[memory-vec] update skipped memory_id=${id} reason=invalid_dimensions dimensions=${embedding.length} expected=${EMBEDDING_DIMENSIONS}`,
|
|
1312
|
+
);
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
try {
|
|
1316
|
+
await tx.run("DELETE FROM memory_vec WHERE memory_id = ?", [id]);
|
|
1317
|
+
await tx.run("INSERT INTO memory_vec(memory_id, embedding) VALUES (?, ?)", [
|
|
1318
|
+
id,
|
|
1319
|
+
vecBuffer,
|
|
1320
|
+
]);
|
|
1321
|
+
} catch (err) {
|
|
1322
|
+
console.error(`[memory-vec] update failed memory_id=${id}: ${(err as Error).message}`);
|
|
1323
|
+
}
|
|
1278
1324
|
}
|
|
1279
|
-
}
|
|
1325
|
+
});
|
|
1280
1326
|
}
|
|
1281
1327
|
|
|
1282
1328
|
async rewriteForScrub(
|
|
@@ -160,5 +160,35 @@ export function rerank(candidates: MemoryCandidate[], options: RerankOptions): M
|
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
scored.sort((a, b) => b.similarity - a.similarity || b.rawSimilarity - a.rawSimilarity);
|
|
163
|
-
return scored.slice(0, limit);
|
|
163
|
+
return collapseTaskCompletionChunks(scored).slice(0, limit);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The document rerank() collapses a candidate into, or null when the candidate
|
|
168
|
+
* is ranked on its own (anything but a chunk of a multi-chunk task memory).
|
|
169
|
+
*/
|
|
170
|
+
export function collapsedDocumentKey(
|
|
171
|
+
candidate: Pick<MemoryCandidate, "source" | "totalChunks" | "key" | "agentId" | "scope">,
|
|
172
|
+
): string | null {
|
|
173
|
+
if (candidate.source !== "task_completion" || candidate.totalChunks <= 1 || !candidate.key) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return JSON.stringify([candidate.agentId, candidate.scope, candidate.key]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Keep only the best-ranked chunk of each multi-chunk task_completion memory.
|
|
181
|
+
* One long task output splits into a hundred chunks or more, which would
|
|
182
|
+
* otherwise fill every result slot. Chunks of one task share a key (see
|
|
183
|
+
* taskCompletionMemoryKey), scoped by owner and visibility.
|
|
184
|
+
*/
|
|
185
|
+
function collapseTaskCompletionChunks<T extends MemoryCandidate>(ranked: T[]): T[] {
|
|
186
|
+
const seen = new Set<string>();
|
|
187
|
+
return ranked.filter((candidate) => {
|
|
188
|
+
const document = collapsedDocumentKey(candidate);
|
|
189
|
+
if (document === null) return true;
|
|
190
|
+
if (seen.has(document)) return false;
|
|
191
|
+
seen.add(document);
|
|
192
|
+
return true;
|
|
193
|
+
});
|
|
164
194
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Candidate fetch for memory search, sized in documents rather than rows.
|
|
3
|
+
*
|
|
4
|
+
* rerank() collapses the chunks of one task_completion document into its best
|
|
5
|
+
* chunk. When those chunks fill the candidate window, the collapse leaves
|
|
6
|
+
* fewer documents than the caller asked for while other matches sit just past
|
|
7
|
+
* the cutoff. This fetch widens the window until it holds `limit` documents,
|
|
8
|
+
* or the store has nothing more to return.
|
|
9
|
+
*/
|
|
10
|
+
import { collapsedDocumentKey } from "./reranker";
|
|
11
|
+
import type { MemoryCandidate, MemorySearchOptions, MemoryStore } from "./types";
|
|
12
|
+
|
|
13
|
+
/** Upper bound on one widened fetch. */
|
|
14
|
+
export const MAX_CANDIDATE_FETCH = 4096;
|
|
15
|
+
const WIDEN_FACTOR = 4;
|
|
16
|
+
|
|
17
|
+
export function countCandidateDocuments(candidates: MemoryCandidate[]): number {
|
|
18
|
+
const documents = new Set<string>();
|
|
19
|
+
let standalone = 0;
|
|
20
|
+
for (const candidate of candidates) {
|
|
21
|
+
const document = collapsedDocumentKey(candidate);
|
|
22
|
+
if (document === null) standalone++;
|
|
23
|
+
else documents.add(document);
|
|
24
|
+
}
|
|
25
|
+
return standalone + documents.size;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function searchMemoryCandidates(
|
|
29
|
+
store: MemoryStore,
|
|
30
|
+
embedding: Float32Array,
|
|
31
|
+
agentId: string,
|
|
32
|
+
options: MemorySearchOptions & { limit: number },
|
|
33
|
+
): Promise<MemoryCandidate[]> {
|
|
34
|
+
let fetchLimit = options.limit;
|
|
35
|
+
let rows = await store.search(embedding, agentId, { ...options, limit: fetchLimit });
|
|
36
|
+
while (fetchLimit < MAX_CANDIDATE_FETCH) {
|
|
37
|
+
const documents = countCandidateDocuments(rows);
|
|
38
|
+
// Enough documents, or no chunk duplicates for the collapse to remove.
|
|
39
|
+
if (documents >= options.limit || documents === rows.length) break;
|
|
40
|
+
const nextLimit = Math.min(fetchLimit * WIDEN_FACTOR, MAX_CANDIDATE_FETCH);
|
|
41
|
+
const wider = await store.search(embedding, agentId, { ...options, limit: nextLimit });
|
|
42
|
+
if (wider.length <= rows.length) break; // the store is exhausted
|
|
43
|
+
rows = wider;
|
|
44
|
+
fetchLimit = nextLimit;
|
|
45
|
+
}
|
|
46
|
+
return rows;
|
|
47
|
+
}
|
package/src/be/memory/types.ts
CHANGED
|
@@ -20,6 +20,12 @@ export interface EmbeddingProvider {
|
|
|
20
20
|
export interface MemoryStore {
|
|
21
21
|
store(input: MemoryInput): Promise<AgentMemory>;
|
|
22
22
|
storeBatch(inputs: MemoryInput[]): Promise<AgentMemory[]>;
|
|
23
|
+
/**
|
|
24
|
+
* In one transaction, delete every row of `document` (same key, scope,
|
|
25
|
+
* owner and source) and insert `inputs`. Rows of other owners, scopes or
|
|
26
|
+
* sources are never touched.
|
|
27
|
+
*/
|
|
28
|
+
replaceDocument(document: MemoryDocumentRef, inputs: MemoryInput[]): Promise<AgentMemory[]>;
|
|
23
29
|
get(id: string): Promise<AgentMemory | null>;
|
|
24
30
|
peek(id: string): Promise<AgentMemory | null>;
|
|
25
31
|
search(
|
|
@@ -79,6 +85,14 @@ export interface MemoryInput {
|
|
|
79
85
|
key?: string | null;
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
/** The rows of one keyed document: every chunk shares these four fields. */
|
|
89
|
+
export interface MemoryDocumentRef {
|
|
90
|
+
key: string;
|
|
91
|
+
scope: AgentMemoryScope;
|
|
92
|
+
agentId: string | null;
|
|
93
|
+
source: AgentMemorySource;
|
|
94
|
+
}
|
|
95
|
+
|
|
82
96
|
export interface MemoryCandidate extends AgentMemory {
|
|
83
97
|
similarity: number;
|
|
84
98
|
/** Raw cosine similarity before reranking (preserved for diagnostics). */
|