@retinue/agentkit 0.2.0 → 0.3.1
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/README.md +1 -1
- package/dist/adapters/audio/openai.d.ts +36 -0
- package/dist/adapters/audio/openai.js +136 -0
- package/dist/adapters/bullmq/lock.d.ts +26 -2
- package/dist/adapters/memory/connections.d.ts +10 -0
- package/dist/adapters/memory/connections.js +87 -0
- package/dist/adapters/memory/graph.d.ts +17 -0
- package/dist/adapters/memory/graph.js +267 -0
- package/dist/adapters/memory/index.d.ts +3 -0
- package/dist/adapters/memory/index.js +3 -0
- package/dist/adapters/memory/rate-limit.d.ts +14 -0
- package/dist/adapters/memory/rate-limit.js +28 -0
- package/dist/adapters/memory/runtime.js +0 -0
- package/dist/adapters/postgres/connections.d.ts +15 -0
- package/dist/adapters/postgres/connections.js +134 -0
- package/dist/adapters/postgres/graph.d.ts +24 -0
- package/dist/adapters/postgres/graph.js +325 -0
- package/dist/adapters/postgres/index.d.ts +2 -0
- package/dist/adapters/postgres/index.js +2 -0
- package/dist/adapters/postgres/migrations.js +198 -0
- package/dist/adapters/postgres/run-store.js +26 -2
- package/dist/adapters/postgres/schema.d.ts +61 -2
- package/dist/adapters/postgres/schema.js +95 -10
- package/dist/adapters/redis/index.d.ts +1 -0
- package/dist/adapters/redis/index.js +1 -0
- package/dist/adapters/redis/rate-limit.d.ts +46 -0
- package/dist/adapters/redis/rate-limit.js +55 -0
- package/dist/adapters/supabase/index.d.ts +15 -0
- package/dist/adapters/supabase/index.js +15 -0
- package/dist/adapters/supabase/rls.js +33 -0
- package/dist/agents/agent.d.ts +78 -1
- package/dist/agents/agent.js +118 -5
- package/dist/agents/define.js +24 -4
- package/dist/agents/engine.d.ts +20 -0
- package/dist/agents/engine.js +177 -6
- package/dist/audio/index.d.ts +151 -0
- package/dist/audio/index.js +183 -0
- package/dist/connections/cipher.d.ts +103 -0
- package/dist/connections/cipher.js +141 -0
- package/dist/connections/index.d.ts +156 -0
- package/dist/connections/index.js +72 -0
- package/dist/connections/oauth/client.d.ts +84 -0
- package/dist/connections/oauth/client.js +141 -0
- package/dist/connections/oauth/index.d.ts +166 -0
- package/dist/connections/oauth/index.js +226 -0
- package/dist/connections/oauth/service.d.ts +90 -0
- package/dist/connections/oauth/service.js +132 -0
- package/dist/connections/pause.d.ts +68 -0
- package/dist/connections/pause.js +97 -0
- package/dist/connections/resolver.d.ts +59 -0
- package/dist/connections/resolver.js +138 -0
- package/dist/connections/resume.d.ts +64 -0
- package/dist/connections/resume.js +99 -0
- package/dist/connections/vault-cipher.d.ts +95 -0
- package/dist/connections/vault-cipher.js +206 -0
- package/dist/core/content-parts.d.ts +18 -2
- package/dist/core/content-parts.js +1 -0
- package/dist/core/context.d.ts +25 -0
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/errors.js +15 -0
- package/dist/core/events.d.ts +44 -2
- package/dist/core/events.js +31 -2
- package/dist/core/validation.js +19 -0
- package/dist/entries/connections.d.ts +10 -0
- package/dist/entries/connections.js +10 -0
- package/dist/entries/knowledge.d.ts +8 -0
- package/dist/entries/knowledge.js +8 -0
- package/dist/entries/mcp-server.d.ts +9 -0
- package/dist/entries/mcp-server.js +9 -0
- package/dist/entries/runtime.d.ts +10 -0
- package/dist/entries/runtime.js +8 -0
- package/dist/entries/testing.d.ts +28 -0
- package/dist/entries/testing.js +28 -0
- package/dist/graphql/resolvers.d.ts +3 -1
- package/dist/graphql/resolvers.js +14 -0
- package/dist/knowledge/communities.d.ts +166 -0
- package/dist/knowledge/communities.js +377 -0
- package/dist/knowledge/graph-global.d.ts +153 -0
- package/dist/knowledge/graph-global.js +246 -0
- package/dist/knowledge/graph-retrieval.d.ts +150 -0
- package/dist/knowledge/graph-retrieval.js +303 -0
- package/dist/knowledge/graph.d.ts +223 -0
- package/dist/knowledge/graph.js +369 -0
- package/dist/knowledge/index.d.ts +24 -0
- package/dist/knowledge/index.js +36 -1
- package/dist/knowledge/retrieval.d.ts +26 -1
- package/dist/knowledge/retrieval.js +77 -1
- package/dist/mcp-server/index.d.ts +168 -0
- package/dist/mcp-server/index.js +175 -0
- package/dist/models/extraction.d.ts +73 -0
- package/dist/models/extraction.js +118 -0
- package/dist/models/index.d.ts +40 -1
- package/dist/models/index.js +22 -1
- package/dist/models/pricing.d.ts +2 -0
- package/dist/models/pricing.js +17 -1
- package/dist/models/provider-factory.js +9 -7
- package/dist/models/streaming.d.ts +89 -0
- package/dist/models/streaming.js +179 -2
- package/dist/persistence/index.d.ts +230 -0
- package/dist/persistence/index.js +2 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +12 -0
- package/dist/runtime/worker.d.ts +36 -1
- package/dist/runtime/worker.js +37 -4
- package/dist/security/checklist.js +46 -0
- package/dist/server/bin.d.ts +72 -0
- package/dist/server/bin.js +304 -0
- package/dist/server/boot.d.ts +20 -2
- package/dist/server/boot.js +29 -3
- package/dist/server/cli-worker.js +6 -3
- package/dist/server/cli.d.ts +13 -0
- package/dist/server/cli.js +10 -5
- package/dist/server/config.d.ts +11 -1
- package/dist/server/config.js +18 -0
- package/dist/server/doctor.d.ts +85 -0
- package/dist/server/doctor.js +280 -0
- package/dist/server/pool.d.ts +47 -0
- package/dist/server/pool.js +73 -0
- package/dist/skills/context.d.ts +61 -0
- package/dist/skills/context.js +107 -0
- package/dist/skills/index.d.ts +1 -0
- package/dist/skills/index.js +1 -0
- package/dist/telemetry/spans.js +5 -0
- package/dist/testing/conformance/artifact-exports.d.ts +20 -0
- package/dist/testing/conformance/artifact-exports.js +172 -0
- package/dist/testing/conformance/artifacts.d.ts +11 -0
- package/dist/testing/conformance/artifacts.js +316 -0
- package/dist/testing/conformance/audio.d.ts +38 -0
- package/dist/testing/conformance/audio.js +145 -0
- package/dist/testing/conformance/capability.d.ts +23 -0
- package/dist/testing/conformance/capability.js +27 -0
- package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
- package/dist/testing/conformance/checkpoint-store.js +72 -0
- package/dist/testing/conformance/connections.d.ts +14 -0
- package/dist/testing/conformance/connections.js +171 -0
- package/dist/testing/conformance/conversation-store.d.ts +10 -0
- package/dist/testing/conformance/conversation-store.js +60 -0
- package/dist/testing/conformance/evaluation.d.ts +10 -0
- package/dist/testing/conformance/evaluation.js +295 -0
- package/dist/testing/conformance/files.d.ts +19 -0
- package/dist/testing/conformance/files.js +454 -0
- package/dist/testing/conformance/flows.d.ts +16 -0
- package/dist/testing/conformance/flows.js +193 -0
- package/dist/testing/conformance/graph.d.ts +22 -0
- package/dist/testing/conformance/graph.js +500 -0
- package/dist/testing/conformance/hitl.d.ts +25 -0
- package/dist/testing/conformance/hitl.js +523 -0
- package/dist/testing/conformance/index.d.ts +163 -0
- package/dist/testing/conformance/index.js +321 -0
- package/dist/testing/conformance/invariants.d.ts +23 -0
- package/dist/testing/conformance/invariants.js +80 -0
- package/dist/testing/conformance/knowledge.d.ts +41 -0
- package/dist/testing/conformance/knowledge.js +592 -0
- package/dist/testing/conformance/parents.d.ts +50 -0
- package/dist/testing/conformance/parents.js +39 -0
- package/dist/testing/conformance/rate-limit.d.ts +25 -0
- package/dist/testing/conformance/rate-limit.js +71 -0
- package/dist/testing/conformance/records.d.ts +36 -0
- package/dist/testing/conformance/records.js +400 -0
- package/dist/testing/conformance/rollups.d.ts +30 -0
- package/dist/testing/conformance/rollups.js +425 -0
- package/dist/testing/conformance/run-coordinator.d.ts +27 -0
- package/dist/testing/conformance/run-coordinator.js +150 -0
- package/dist/testing/conformance/run-event-log.d.ts +9 -0
- package/dist/testing/conformance/run-event-log.js +138 -0
- package/dist/testing/conformance/run-store.d.ts +12 -0
- package/dist/testing/conformance/run-store.js +294 -0
- package/dist/testing/conformance/session-state.d.ts +38 -0
- package/dist/testing/conformance/session-state.js +198 -0
- package/dist/testing/conformance/usage-limits.d.ts +10 -0
- package/dist/testing/conformance/usage-limits.js +309 -0
- package/dist/testing/conformance.d.ts +8 -0
- package/dist/testing/conformance.js +8 -0
- package/dist/testing/memory-backend.d.ts +39 -0
- package/dist/testing/memory-backend.js +41 -0
- package/dist/testing/pglite.d.ts +55 -0
- package/dist/testing/pglite.js +78 -0
- package/dist/testing/stub-model.d.ts +58 -0
- package/dist/testing/stub-model.js +71 -0
- package/dist/testing/supabase-storage-double.d.ts +35 -0
- package/dist/testing/supabase-storage-double.js +101 -0
- package/dist/toolkit/http.d.ts +27 -0
- package/dist/toolkit/http.js +48 -0
- package/dist/toolkit/index.d.ts +27 -1
- package/dist/toolkit/index.js +24 -1
- package/dist/toolkit/mime.d.ts +97 -0
- package/dist/toolkit/mime.js +233 -0
- package/dist/toolkit/ssrf.d.ts +139 -0
- package/dist/toolkit/ssrf.js +349 -0
- package/dist/toolkit/vendor.d.ts +91 -0
- package/dist/toolkit/vendor.js +153 -0
- package/dist/tools/credentials.d.ts +265 -6
- package/dist/tools/credentials.js +318 -3
- package/dist/tools/define.d.ts +14 -0
- package/dist/tools/define.js +3 -0
- package/dist/tools/index.d.ts +21 -0
- package/dist/tools/index.js +7 -0
- package/dist/tools/library/audio.d.ts +63 -0
- package/dist/tools/library/audio.js +148 -0
- package/dist/tools/library/index.d.ts +8 -0
- package/dist/tools/library/index.js +7 -0
- package/dist/tools/registry.d.ts +33 -0
- package/dist/tools/registry.js +72 -2
- package/dist/usage/index.d.ts +21 -0
- package/dist/usage/index.js +1 -0
- package/dist/usage/rate-limit.d.ts +122 -0
- package/dist/usage/rate-limit.js +131 -0
- package/dist/usage/recorder.js +3 -0
- package/package.json +29 -2
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ your own model catalogue when you need them — same code above.
|
|
|
60
60
|
| **Injection containment** | Untrusted content is wrapped in a nonce-delimited envelope with delimiter forgery neutralised — structural, not a detector |
|
|
61
61
|
| **Usage you can bill** | Per-model, per-principal token and cost accounting, with quotas enforced before a run is admitted |
|
|
62
62
|
| **Flows and teams** | Durable multi-step workflows; a team compiles to a flow, and each member's turn is a child run with its own ceiling |
|
|
63
|
-
| **Replaceable everything** |
|
|
63
|
+
| **Replaceable everything** | 32 ports, three adapter families, one conformance suite held over all of them — [exported](https://docs.retinue.riseexperts.de/docs/getting-started/testing), so your adapter is held to it too |
|
|
64
64
|
|
|
65
65
|
## Composing it yourself
|
|
66
66
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI transcription and speech — REQ-062 (#257), task #258, AC-5 and AC-6.
|
|
3
|
+
*
|
|
4
|
+
* One adapter each, which is what the ACs ask for. Written against the HTTP API rather than through the AI SDK
|
|
5
|
+
* because the SDK's audio surface is narrower than the endpoints are: `verbose_json` carries the **duration**,
|
|
6
|
+
* and duration is what usage is charged on. An adapter that could not report it would under-bill silently,
|
|
7
|
+
* which AC-8 names as the failure to avoid.
|
|
8
|
+
*
|
|
9
|
+
* ## Where this lives, and why it is exempt from two audits
|
|
10
|
+
*
|
|
11
|
+
* `adapters/audio/`, beside `adapters/embeddings/openai.ts`, which is the same shape: an **operator-configured**
|
|
12
|
+
* provider endpoint that a model never names. Both audits that fire on this file — the raw `apiKey` field and
|
|
13
|
+
* the direct `fetch` — are exempted for that adapter with that reason, and this is exempted alongside it rather
|
|
14
|
+
* than by loosening either check.
|
|
15
|
+
*
|
|
16
|
+
* ## Credentials come in, never out of the environment
|
|
17
|
+
*
|
|
18
|
+
* The same rule every toolkit follows: a provider that read `process.env` could serve one tenant, whoever
|
|
19
|
+
* booted the process. The key is a constructor argument, and a deployment resolves it however it resolves the
|
|
20
|
+
* rest.
|
|
21
|
+
*/
|
|
22
|
+
import type { SpeechProvider, TranscriptionProvider } from "../../audio/index.js";
|
|
23
|
+
export type OpenAiAudioConfig = {
|
|
24
|
+
readonly apiKey: string;
|
|
25
|
+
readonly baseUrl?: string;
|
|
26
|
+
/** `whisper-1` is the one every account has. A deployment on a newer model names it. */
|
|
27
|
+
readonly model?: string;
|
|
28
|
+
readonly fetchImpl?: typeof fetch;
|
|
29
|
+
/** A ceiling on the wait, because a long recording is a long request and the default is none. */
|
|
30
|
+
readonly timeoutMs?: number;
|
|
31
|
+
};
|
|
32
|
+
export declare const openAiTranscription: (config: OpenAiAudioConfig) => TranscriptionProvider;
|
|
33
|
+
export declare const openAiSpeech: (config: OpenAiAudioConfig & {
|
|
34
|
+
readonly voice?: string;
|
|
35
|
+
}) => SpeechProvider;
|
|
36
|
+
//# sourceMappingURL=openai.d.ts.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI transcription and speech — REQ-062 (#257), task #258, AC-5 and AC-6.
|
|
3
|
+
*
|
|
4
|
+
* One adapter each, which is what the ACs ask for. Written against the HTTP API rather than through the AI SDK
|
|
5
|
+
* because the SDK's audio surface is narrower than the endpoints are: `verbose_json` carries the **duration**,
|
|
6
|
+
* and duration is what usage is charged on. An adapter that could not report it would under-bill silently,
|
|
7
|
+
* which AC-8 names as the failure to avoid.
|
|
8
|
+
*
|
|
9
|
+
* ## Where this lives, and why it is exempt from two audits
|
|
10
|
+
*
|
|
11
|
+
* `adapters/audio/`, beside `adapters/embeddings/openai.ts`, which is the same shape: an **operator-configured**
|
|
12
|
+
* provider endpoint that a model never names. Both audits that fire on this file — the raw `apiKey` field and
|
|
13
|
+
* the direct `fetch` — are exempted for that adapter with that reason, and this is exempted alongside it rather
|
|
14
|
+
* than by loosening either check.
|
|
15
|
+
*
|
|
16
|
+
* ## Credentials come in, never out of the environment
|
|
17
|
+
*
|
|
18
|
+
* The same rule every toolkit follows: a provider that read `process.env` could serve one tenant, whoever
|
|
19
|
+
* booted the process. The key is a constructor argument, and a deployment resolves it however it resolves the
|
|
20
|
+
* rest.
|
|
21
|
+
*/
|
|
22
|
+
import { AudioRejected, boundTranscript, failAudio, isAudioMediaType, MAX_SPEECH_CHARS } from "../../audio/index.js";
|
|
23
|
+
const DEFAULT_BASE = "https://api.openai.com/v1";
|
|
24
|
+
/** A status carried on the error, so `describeAudioFailure` can classify it. */
|
|
25
|
+
class HttpError extends Error {
|
|
26
|
+
status;
|
|
27
|
+
constructor(status, message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.status = status;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const extensionFor = (mediaType) => {
|
|
33
|
+
const base = mediaType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
34
|
+
return ({
|
|
35
|
+
"audio/mpeg": "mp3",
|
|
36
|
+
"audio/mp4": "mp4",
|
|
37
|
+
"audio/x-m4a": "m4a",
|
|
38
|
+
"audio/wav": "wav",
|
|
39
|
+
"audio/webm": "webm",
|
|
40
|
+
"audio/ogg": "ogg",
|
|
41
|
+
"audio/flac": "flac",
|
|
42
|
+
}[base] ?? "bin");
|
|
43
|
+
};
|
|
44
|
+
const send = async (config, path, body, headers = {}) => {
|
|
45
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
46
|
+
/**
|
|
47
|
+
* A timeout, because the default is none.
|
|
48
|
+
*
|
|
49
|
+
* The eval harness lost 35 minutes of a paid run to a `fetch` with no timeout, and a transcription request
|
|
50
|
+
* is longer-lived than most — so the omission would be less visible here and cost more.
|
|
51
|
+
*/
|
|
52
|
+
const signal = AbortSignal.timeout(config.timeoutMs ?? 120_000);
|
|
53
|
+
const response = await fetchImpl(`${(config.baseUrl ?? DEFAULT_BASE).replace(/\/$/, "")}${path}`, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { authorization: `Bearer ${config.apiKey}`, ...headers },
|
|
56
|
+
body,
|
|
57
|
+
signal,
|
|
58
|
+
});
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
const detail = await response.text().catch(() => "");
|
|
61
|
+
throw new HttpError(response.status, detail.slice(0, 400) || response.statusText);
|
|
62
|
+
}
|
|
63
|
+
return response;
|
|
64
|
+
};
|
|
65
|
+
export const openAiTranscription = (config) => ({
|
|
66
|
+
id: "openai",
|
|
67
|
+
async transcribe(request) {
|
|
68
|
+
// Checked here as well as at the boundary: this provider can be called directly, and a media type the API
|
|
69
|
+
// rejects produces a message about the request rather than about the format.
|
|
70
|
+
if (!isAudioMediaType(request.mediaType)) {
|
|
71
|
+
failAudio(new AudioRejected(`${request.mediaType} is not an audio format this adapter accepts.`), "openai");
|
|
72
|
+
}
|
|
73
|
+
const form = new FormData();
|
|
74
|
+
form.set("file", new Blob([request.audio], { type: request.mediaType }), request.filename ?? `audio.${extensionFor(request.mediaType)}`);
|
|
75
|
+
form.set("model", config.model ?? "whisper-1");
|
|
76
|
+
/**
|
|
77
|
+
* `verbose_json`, for the duration — AC-8.
|
|
78
|
+
*
|
|
79
|
+
* `json` returns the text alone. Audio is billed per second, so an adapter that asked for `json` would
|
|
80
|
+
* make every transcription cost nothing in the ledger: the recorder charges on `durationSeconds`, and
|
|
81
|
+
* absent means zero. That is the silent revenue hole the AC names, and this one field is the difference.
|
|
82
|
+
*/
|
|
83
|
+
form.set("response_format", "verbose_json");
|
|
84
|
+
if (request.languageHint !== undefined)
|
|
85
|
+
form.set("language", request.languageHint);
|
|
86
|
+
try {
|
|
87
|
+
const response = await send(config, "/audio/transcriptions", form);
|
|
88
|
+
const payload = (await response.json());
|
|
89
|
+
const bounded = boundTranscript(typeof payload.text === "string" ? payload.text : "", request.maxTranscriptChars);
|
|
90
|
+
return {
|
|
91
|
+
text: bounded.text,
|
|
92
|
+
truncated: bounded.truncated,
|
|
93
|
+
...(typeof payload.duration === "number" ? { durationSeconds: payload.duration } : {}),
|
|
94
|
+
...(typeof payload.language === "string" ? { language: payload.language } : {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
return failAudio(error, "openai");
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
export const openAiSpeech = (config) => ({
|
|
103
|
+
id: "openai",
|
|
104
|
+
async speak(request) {
|
|
105
|
+
const text = request.text.trim();
|
|
106
|
+
if (text === "")
|
|
107
|
+
failAudio(new AudioRejected("There is nothing to say — the text is empty."), "openai");
|
|
108
|
+
if (text.length > MAX_SPEECH_CHARS) {
|
|
109
|
+
failAudio(new AudioRejected(`That is ${text.length} characters and the limit is ${MAX_SPEECH_CHARS}. Refused before the call, ` +
|
|
110
|
+
"because speech is billed per character and a runaway prompt is a runaway bill."), "openai");
|
|
111
|
+
}
|
|
112
|
+
const format = request.format ?? "mp3";
|
|
113
|
+
try {
|
|
114
|
+
const response = await send(config, "/audio/speech", JSON.stringify({
|
|
115
|
+
model: config.model ?? "tts-1",
|
|
116
|
+
input: text,
|
|
117
|
+
voice: request.voice ?? config.voice ?? "alloy",
|
|
118
|
+
response_format: format,
|
|
119
|
+
}), { "content-type": "application/json" });
|
|
120
|
+
const audio = new Uint8Array(await response.arrayBuffer());
|
|
121
|
+
return {
|
|
122
|
+
audio,
|
|
123
|
+
mediaType: { mp3: "audio/mpeg", wav: "audio/wav", opus: "audio/ogg", flac: "audio/flac" }[format],
|
|
124
|
+
/**
|
|
125
|
+
* No duration. The endpoint does not report one, and computing it would mean decoding the audio —
|
|
126
|
+
* so it is absent rather than guessed, and speech is charged per character instead. Stated because an
|
|
127
|
+
* absent field that looked like an oversight would invite somebody to estimate it.
|
|
128
|
+
*/
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
return failAudio(error, "openai");
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
//# sourceMappingURL=openai.js.map
|
|
@@ -43,8 +43,32 @@ export type RedisLockOptions = {
|
|
|
43
43
|
/** Injectable for tests. Production uses `crypto.randomUUID`. */
|
|
44
44
|
readonly newToken?: () => string;
|
|
45
45
|
};
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
/**
|
|
47
|
+
* What this adapter's handle actually carries — declared, because it was not.
|
|
48
|
+
*
|
|
49
|
+
* `acquire` was typed as returning the port's `LockHandle`, which is `{ released }` and nothing else, while the
|
|
50
|
+
* implementation returned `token`, `key` and `renew` as well. Three real capabilities, invisible to every typed
|
|
51
|
+
* caller: `renew` in particular is the whole lease-extension mechanism and could not be called at all without
|
|
52
|
+
* casting. The port stays narrow on purpose — a Postgres advisory lock has no token or namespaced key in this
|
|
53
|
+
* sense — so the *adapter* is the right place to say what it adds.
|
|
54
|
+
*/
|
|
55
|
+
export type RedisLockHandle = LockHandle & {
|
|
56
|
+
/** Per acquisition, never per store — see `newToken`. Releasing checks it, so it is the ownership proof. */
|
|
57
|
+
readonly token: string;
|
|
58
|
+
/** The namespaced key, so a lock cannot collide with the queue's own keys in a shared Redis. */
|
|
59
|
+
readonly key: string;
|
|
60
|
+
/** Extends the lease. Returns false once released, and false if another holder has taken the key. */
|
|
61
|
+
renew(nextTtlMs?: number): Promise<boolean>;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* `Omit` rather than an intersection, and the difference is not cosmetic.
|
|
65
|
+
*
|
|
66
|
+
* `DistributedLockStore & { acquire(...) }` makes `acquire` an *overload set*, and a call resolves against the
|
|
67
|
+
* first signature — the port's, returning the narrow `LockHandle`. So the widened return type was written,
|
|
68
|
+
* compiled, and had no effect on any caller. Omitting the member first replaces it instead of adding to it.
|
|
69
|
+
*/
|
|
70
|
+
export type RedisLockStore = Omit<DistributedLockStore, "acquire"> & {
|
|
71
|
+
acquire(key: string, ttlMs: number): Promise<RedisLockHandle | null>;
|
|
48
72
|
};
|
|
49
73
|
export declare const createRedisLockStore: (redis: LockRedis, options?: RedisLockOptions) => RedisLockStore;
|
|
50
74
|
/**
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reference `ConnectionStore` — task #261.
|
|
3
|
+
*
|
|
4
|
+
* The baseline the other adapters are compared against, and the one the conformance suite runs first. It holds
|
|
5
|
+
* `SealedSecret`s exactly as Postgres does: an in-memory adapter that stored plaintext would make the raw-row
|
|
6
|
+
* assertion pass for the wrong reason, and the suite would then be asserting a different guarantee per adapter.
|
|
7
|
+
*/
|
|
8
|
+
import type { ConnectionStore } from "../../connections/index.js";
|
|
9
|
+
export declare const createMemoryConnectionStore: (now?: () => string) => ConnectionStore;
|
|
10
|
+
//# sourceMappingURL=connections.d.ts.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reference `ConnectionStore` — task #261.
|
|
3
|
+
*
|
|
4
|
+
* The baseline the other adapters are compared against, and the one the conformance suite runs first. It holds
|
|
5
|
+
* `SealedSecret`s exactly as Postgres does: an in-memory adapter that stored plaintext would make the raw-row
|
|
6
|
+
* assertion pass for the wrong reason, and the suite would then be asserting a different guarantee per adapter.
|
|
7
|
+
*/
|
|
8
|
+
import { AgentPlatformError } from "../../core/errors.js";
|
|
9
|
+
const notFound = (id) => new AgentPlatformError({ code: "not_found", message: `no connection "${id}" for this tenant`, retryable: false });
|
|
10
|
+
export const createMemoryConnectionStore = (now = () => new Date().toISOString()) => {
|
|
11
|
+
// Keyed by tenant, so a missing tenant filter is a missing map rather than a leak.
|
|
12
|
+
const byTenant = new Map();
|
|
13
|
+
const of = (tenantId) => {
|
|
14
|
+
const existing = byTenant.get(tenantId);
|
|
15
|
+
if (existing !== undefined)
|
|
16
|
+
return existing;
|
|
17
|
+
const created = new Map();
|
|
18
|
+
byTenant.set(tenantId, created);
|
|
19
|
+
return created;
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
async create({ tenantId, connection }) {
|
|
23
|
+
const rows = of(tenantId);
|
|
24
|
+
if (rows.has(connection.id))
|
|
25
|
+
throw new AgentPlatformError({
|
|
26
|
+
code: "conflict",
|
|
27
|
+
message: `connection "${connection.id}" already exists for this tenant`,
|
|
28
|
+
retryable: false,
|
|
29
|
+
});
|
|
30
|
+
const at = now();
|
|
31
|
+
const row = { ...connection, createdAt: at, updatedAt: at };
|
|
32
|
+
rows.set(connection.id, row);
|
|
33
|
+
return row;
|
|
34
|
+
},
|
|
35
|
+
async get({ tenantId, id, kind }) {
|
|
36
|
+
const row = of(tenantId).get(id);
|
|
37
|
+
// A caller that did not ask for an app registration must not receive one — see `ConnectionKind`.
|
|
38
|
+
if (row !== undefined && (row.kind ?? "connection") !== (kind ?? "connection"))
|
|
39
|
+
return null;
|
|
40
|
+
// A revoked connection reads as absent. Anything else would let a caller resolve a credential somebody
|
|
41
|
+
// deliberately withdrew.
|
|
42
|
+
return row === undefined || row.revokedAt !== undefined ? null : row;
|
|
43
|
+
},
|
|
44
|
+
async list({ tenantId, provider, kind }) {
|
|
45
|
+
return [...of(tenantId).values()]
|
|
46
|
+
.filter((c) => c.revokedAt === undefined &&
|
|
47
|
+
(provider === undefined || c.provider === provider) &&
|
|
48
|
+
(c.kind ?? "connection") === (kind ?? "connection"))
|
|
49
|
+
.sort((a, b) => (a.createdAt === b.createdAt ? a.id.localeCompare(b.id) : a.createdAt.localeCompare(b.createdAt)));
|
|
50
|
+
},
|
|
51
|
+
async update({ tenantId, id, patch }) {
|
|
52
|
+
const rows = of(tenantId);
|
|
53
|
+
const existing = rows.get(id);
|
|
54
|
+
if (existing === undefined || existing.revokedAt !== undefined)
|
|
55
|
+
throw notFound(id);
|
|
56
|
+
const updated = {
|
|
57
|
+
...existing,
|
|
58
|
+
...(patch.label === undefined ? {} : { label: patch.label }),
|
|
59
|
+
...(patch.sealed === undefined ? {} : { sealed: patch.sealed }),
|
|
60
|
+
...(patch.metadata === undefined ? {} : { metadata: patch.metadata }),
|
|
61
|
+
...(patch.grantedScopes === undefined ? {} : { grantedScopes: patch.grantedScopes }),
|
|
62
|
+
...(patch.expiresAt === undefined ? {} : { expiresAt: patch.expiresAt }),
|
|
63
|
+
updatedAt: now(),
|
|
64
|
+
};
|
|
65
|
+
rows.set(id, updated);
|
|
66
|
+
return updated;
|
|
67
|
+
},
|
|
68
|
+
async revoke({ tenantId, id }) {
|
|
69
|
+
const rows = of(tenantId);
|
|
70
|
+
const existing = rows.get(id);
|
|
71
|
+
if (existing === undefined)
|
|
72
|
+
throw notFound(id);
|
|
73
|
+
// Idempotent: revoking twice is not an error, because a caller retrying a disconnect should not have to
|
|
74
|
+
// distinguish "already gone" from "failed".
|
|
75
|
+
if (existing.revokedAt !== undefined)
|
|
76
|
+
return;
|
|
77
|
+
rows.set(id, { ...existing, revokedAt: now(), updatedAt: now() });
|
|
78
|
+
},
|
|
79
|
+
async purge({ tenantId }) {
|
|
80
|
+
const rows = byTenant.get(tenantId);
|
|
81
|
+
const count = rows?.size ?? 0;
|
|
82
|
+
byTenant.delete(tenantId);
|
|
83
|
+
return count;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=connections.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory `GraphStore` — the reference implementation (REQ-064 #270, task #271).
|
|
3
|
+
*
|
|
4
|
+
* The same standard the in-memory `KnowledgeStore` holds itself to: this is not a stub that makes tests pass,
|
|
5
|
+
* it is the definition of the contract that the Postgres adapter is then held to by the same conformance suite.
|
|
6
|
+
* Three things it takes as seriously as a database would:
|
|
7
|
+
*
|
|
8
|
+
* - **Tenant isolation is structural.** Every map is keyed by tenant first, so cross-tenant reads are not
|
|
9
|
+
* prevented by a filter somebody could forget — there is nowhere for another tenant's rows to be.
|
|
10
|
+
* - **Provenance is enforced, not assumed.** A row with no chunks is refused here, so the refusal is part of
|
|
11
|
+
* the contract rather than a Postgres constraint that the reference adapter quietly tolerates.
|
|
12
|
+
* - **A source's contribution is what is replaced.** Entities are shared between sources; re-indexing one
|
|
13
|
+
* document withdraws its claims and prunes only what nothing else asserts.
|
|
14
|
+
*/
|
|
15
|
+
import type { GraphStore } from "../../persistence/index.js";
|
|
16
|
+
export declare const createMemoryGraphStore: () => GraphStore;
|
|
17
|
+
//# sourceMappingURL=graph.d.ts.map
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory `GraphStore` — the reference implementation (REQ-064 #270, task #271).
|
|
3
|
+
*
|
|
4
|
+
* The same standard the in-memory `KnowledgeStore` holds itself to: this is not a stub that makes tests pass,
|
|
5
|
+
* it is the definition of the contract that the Postgres adapter is then held to by the same conformance suite.
|
|
6
|
+
* Three things it takes as seriously as a database would:
|
|
7
|
+
*
|
|
8
|
+
* - **Tenant isolation is structural.** Every map is keyed by tenant first, so cross-tenant reads are not
|
|
9
|
+
* prevented by a filter somebody could forget — there is nowhere for another tenant's rows to be.
|
|
10
|
+
* - **Provenance is enforced, not assumed.** A row with no chunks is refused here, so the refusal is part of
|
|
11
|
+
* the contract rather than a Postgres constraint that the reference adapter quietly tolerates.
|
|
12
|
+
* - **A source's contribution is what is replaced.** Entities are shared between sources; re-indexing one
|
|
13
|
+
* document withdraws its claims and prunes only what nothing else asserts.
|
|
14
|
+
*/
|
|
15
|
+
import { AgentPlatformError } from "../../core/errors.js";
|
|
16
|
+
const sourceKey = (sourceType, sourceId) => `${sourceType} ${sourceId}`;
|
|
17
|
+
const DEFAULT_SETTINGS = { enabled: false, updatedAt: "1970-01-01T00:00:00.000Z" };
|
|
18
|
+
const uniqueSorted = (values) => [...new Set(values)].sort();
|
|
19
|
+
const byId = (a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
20
|
+
/**
|
|
21
|
+
* Every contribution, merged into the graph a reader sees.
|
|
22
|
+
*
|
|
23
|
+
* Recomputed on read rather than maintained on write. That is the right trade for a reference adapter: the
|
|
24
|
+
* merge rule then exists in exactly one place, and a bug in it cannot leave a stored graph subtly inconsistent
|
|
25
|
+
* with the contributions it came from. Postgres maintains merged rows for query speed and is held to producing
|
|
26
|
+
* the same answer by `fingerprint`.
|
|
27
|
+
*/
|
|
28
|
+
const mergeAll = (source) => {
|
|
29
|
+
/**
|
|
30
|
+
* Materialised, because this function walks it **twice** — entities, then relationships.
|
|
31
|
+
*
|
|
32
|
+
* The first version took the `Iterable` and looped it directly, which works for an array and is silently
|
|
33
|
+
* wrong for `Map.values()`: an iterator is exhausted by the first pass, so the second saw nothing and *every
|
|
34
|
+
* edge was dropped*. Entities were correct, so the graph looked plausible; only a traversal returned empty.
|
|
35
|
+
* Found by the neighbour-ordering test, and worth the parameter rename so the two loops cannot drift back.
|
|
36
|
+
*/
|
|
37
|
+
const contributions = [...source];
|
|
38
|
+
const entities = new Map();
|
|
39
|
+
const relationships = new Map();
|
|
40
|
+
for (const contribution of contributions) {
|
|
41
|
+
for (const entity of contribution.entities) {
|
|
42
|
+
const existing = entities.get(entity.id);
|
|
43
|
+
if (existing === undefined) {
|
|
44
|
+
entities.set(entity.id, entity);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const description = (entity.description ?? "").length > (existing.description ?? "").length ? entity.description : existing.description;
|
|
48
|
+
entities.set(entity.id, {
|
|
49
|
+
id: entity.id,
|
|
50
|
+
// Lexicographically first, matching `mergeContributions`. "First written" would depend on source order,
|
|
51
|
+
// and source order is a thing that changes without the corpus changing.
|
|
52
|
+
name: existing.name < entity.name ? existing.name : entity.name,
|
|
53
|
+
type: existing.type,
|
|
54
|
+
...(description === undefined ? {} : { description }),
|
|
55
|
+
surfaceForms: uniqueSorted([...existing.surfaceForms, ...entity.surfaceForms]),
|
|
56
|
+
provenance: uniqueSorted([...existing.provenance, ...entity.provenance]),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const contribution of contributions) {
|
|
61
|
+
for (const edge of contribution.relationships) {
|
|
62
|
+
const existing = relationships.get(edge.id);
|
|
63
|
+
if (existing === undefined) {
|
|
64
|
+
relationships.set(edge.id, edge);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const description = (edge.description ?? "").length > (existing.description ?? "").length ? edge.description : existing.description;
|
|
68
|
+
const provenance = uniqueSorted([...existing.provenance, ...edge.provenance]);
|
|
69
|
+
relationships.set(edge.id, {
|
|
70
|
+
...existing,
|
|
71
|
+
...(description === undefined ? {} : { description }),
|
|
72
|
+
weight: provenance.length,
|
|
73
|
+
provenance,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// An edge whose endpoints are gone goes with them — the state after a source is removed.
|
|
78
|
+
const kept = [...relationships.values()].filter((edge) => entities.has(edge.fromId) && entities.has(edge.toId));
|
|
79
|
+
return { entities: [...entities.values()].sort(byId), relationships: kept.sort(byId) };
|
|
80
|
+
};
|
|
81
|
+
/** Refuses an untraceable claim. See `GraphStore` — provenance is structural. */
|
|
82
|
+
const assertProvenance = (contribution) => {
|
|
83
|
+
for (const entity of contribution.entities) {
|
|
84
|
+
if (entity.provenance.length === 0)
|
|
85
|
+
throw new AgentPlatformError({
|
|
86
|
+
code: "invalid_input",
|
|
87
|
+
message: `entity ${entity.id} has no provenance; a graph claim with no chunk behind it is one the model would cite`,
|
|
88
|
+
retryable: false,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
for (const edge of contribution.relationships) {
|
|
92
|
+
if (edge.provenance.length === 0)
|
|
93
|
+
throw new AgentPlatformError({
|
|
94
|
+
code: "invalid_input",
|
|
95
|
+
message: `relationship ${edge.id} has no provenance; a graph claim with no chunk behind it is one the model would cite`,
|
|
96
|
+
retryable: false,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
export const createMemoryGraphStore = () => {
|
|
101
|
+
const tenants = new Map();
|
|
102
|
+
const graphOf = (tenantId) => {
|
|
103
|
+
let graph = tenants.get(tenantId);
|
|
104
|
+
if (!graph) {
|
|
105
|
+
tenants.set(tenantId, (graph = { settings: DEFAULT_SETTINGS, sourceFlags: new Map(), contributions: new Map(), communities: new Map() }));
|
|
106
|
+
}
|
|
107
|
+
return graph;
|
|
108
|
+
};
|
|
109
|
+
const page = (items, limit, cursor) => {
|
|
110
|
+
const offset = cursor === undefined ? 0 : Number.parseInt(cursor, 10) || 0;
|
|
111
|
+
const slice = items.slice(offset, offset + limit);
|
|
112
|
+
const next = offset + slice.length;
|
|
113
|
+
return next < items.length ? { items: slice, nextCursor: String(next) } : { items: slice };
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
async getSettings({ tenantId }) {
|
|
117
|
+
return graphOf(tenantId).settings;
|
|
118
|
+
},
|
|
119
|
+
async setEnabled({ tenantId, enabled, at }) {
|
|
120
|
+
const graph = graphOf(tenantId);
|
|
121
|
+
graph.settings = { enabled, updatedAt: at };
|
|
122
|
+
return graph.settings;
|
|
123
|
+
},
|
|
124
|
+
async setSourceEnabled({ tenantId, sourceType, sourceId, enabled }) {
|
|
125
|
+
// Stored regardless of the tenant switch — AC-2. Marking a handbook today and enabling the tenant next
|
|
126
|
+
// week must not require re-marking anything, so this never consults `settings`.
|
|
127
|
+
graphOf(tenantId).sourceFlags.set(sourceKey(sourceType, sourceId), enabled);
|
|
128
|
+
},
|
|
129
|
+
async isSourceEnabled({ tenantId, sourceType, sourceId }) {
|
|
130
|
+
return graphOf(tenantId).sourceFlags.get(sourceKey(sourceType, sourceId)) === true;
|
|
131
|
+
},
|
|
132
|
+
async listEnabledSources({ tenantId, limit, cursor }) {
|
|
133
|
+
const rows = [...graphOf(tenantId).sourceFlags.entries()]
|
|
134
|
+
.filter(([, enabled]) => enabled)
|
|
135
|
+
.map(([key]) => {
|
|
136
|
+
const at = key.indexOf(" ");
|
|
137
|
+
return { sourceType: key.slice(0, at), sourceId: key.slice(at + 1) };
|
|
138
|
+
})
|
|
139
|
+
.sort((a, b) => (sourceKey(a.sourceType, a.sourceId) < sourceKey(b.sourceType, b.sourceId) ? -1 : 1));
|
|
140
|
+
return page(rows, limit, cursor);
|
|
141
|
+
},
|
|
142
|
+
async replaceSourceGraph({ tenantId, sourceType, sourceId, contribution }) {
|
|
143
|
+
assertProvenance(contribution);
|
|
144
|
+
const graph = graphOf(tenantId);
|
|
145
|
+
const before = mergeAll(graph.contributions.values()).entities.length;
|
|
146
|
+
const key = sourceKey(sourceType, sourceId);
|
|
147
|
+
if (contribution.entities.length === 0 && contribution.relationships.length === 0) {
|
|
148
|
+
graph.contributions.delete(key);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
graph.contributions.set(key, { entities: contribution.entities, relationships: contribution.relationships });
|
|
152
|
+
}
|
|
153
|
+
const after = mergeAll(graph.contributions.values());
|
|
154
|
+
return {
|
|
155
|
+
entities: after.entities.length,
|
|
156
|
+
relationships: after.relationships.length,
|
|
157
|
+
// What this replacement removed from the graph, not what it removed from the source. An operator asking
|
|
158
|
+
// "did re-indexing lose anything" is asking the first question.
|
|
159
|
+
pruned: Math.max(0, before - after.entities.length),
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
async deleteSourceGraph({ tenantId, sourceType, sourceId }) {
|
|
163
|
+
const graph = graphOf(tenantId);
|
|
164
|
+
const before = mergeAll(graph.contributions.values()).entities.length;
|
|
165
|
+
graph.contributions.delete(sourceKey(sourceType, sourceId));
|
|
166
|
+
const after = mergeAll(graph.contributions.values()).entities.length;
|
|
167
|
+
return { pruned: Math.max(0, before - after) };
|
|
168
|
+
},
|
|
169
|
+
async getEntity({ tenantId, id }) {
|
|
170
|
+
return mergeAll(graphOf(tenantId).contributions.values()).entities.find((entity) => entity.id === id) ?? null;
|
|
171
|
+
},
|
|
172
|
+
async getEntities({ tenantId, ids }) {
|
|
173
|
+
const wanted = new Set(ids);
|
|
174
|
+
return mergeAll(graphOf(tenantId).contributions.values()).entities.filter((entity) => wanted.has(entity.id));
|
|
175
|
+
},
|
|
176
|
+
async resolveEntities({ tenantId, normalisedNames }) {
|
|
177
|
+
if (normalisedNames.length === 0)
|
|
178
|
+
return [];
|
|
179
|
+
// The name is everything after the first colon; `normaliseName` cannot produce one, so the split is safe.
|
|
180
|
+
const wanted = new Set(normalisedNames);
|
|
181
|
+
return mergeAll(graphOf(tenantId).contributions.values()).entities.filter((entity) => wanted.has(entity.id.slice(entity.id.indexOf(":") + 1)));
|
|
182
|
+
},
|
|
183
|
+
async listEntities({ tenantId, limit, cursor, type }) {
|
|
184
|
+
const all = mergeAll(graphOf(tenantId).contributions.values()).entities;
|
|
185
|
+
return page(type === undefined ? all : all.filter((entity) => entity.type === type), limit, cursor);
|
|
186
|
+
},
|
|
187
|
+
async neighbours({ tenantId, entityIds, limit }) {
|
|
188
|
+
const wanted = new Set(entityIds);
|
|
189
|
+
const { relationships } = mergeAll(graphOf(tenantId).contributions.values());
|
|
190
|
+
return relationships
|
|
191
|
+
.filter((edge) => wanted.has(edge.fromId) || wanted.has(edge.toId))
|
|
192
|
+
// Heaviest first — the edge the most chunks asserted is the one most worth traversing. Ties broken by
|
|
193
|
+
// id so the order is total, which is what makes a truncated traversal reproducible.
|
|
194
|
+
.sort((a, b) => (b.weight - a.weight) || byId(a, b))
|
|
195
|
+
.slice(0, Math.max(0, limit));
|
|
196
|
+
},
|
|
197
|
+
async replaceCommunities({ tenantId, communities }) {
|
|
198
|
+
const graph = graphOf(tenantId);
|
|
199
|
+
const previous = new Map(graph.communities);
|
|
200
|
+
graph.communities.clear();
|
|
201
|
+
let summariesKept = 0;
|
|
202
|
+
for (const community of communities) {
|
|
203
|
+
const before = previous.get(community.id);
|
|
204
|
+
/**
|
|
205
|
+
* A summary survives a re-clustering **only when the membership is identical**.
|
|
206
|
+
*
|
|
207
|
+
* Carrying it over on id alone would attach a summary of the old members to the new ones — a
|
|
208
|
+
* confidently wrong description of a community that changed, which is worse than having none.
|
|
209
|
+
*/
|
|
210
|
+
const keep = before?.summary !== undefined && before.summaryFingerprint === community.fingerprint;
|
|
211
|
+
if (keep)
|
|
212
|
+
summariesKept += 1;
|
|
213
|
+
graph.communities.set(community.id, {
|
|
214
|
+
...community,
|
|
215
|
+
...(keep && before !== undefined
|
|
216
|
+
? {
|
|
217
|
+
summary: before.summary,
|
|
218
|
+
summaryFingerprint: before.summaryFingerprint,
|
|
219
|
+
...(before.summarisedAt === undefined ? {} : { summarisedAt: before.summarisedAt }),
|
|
220
|
+
}
|
|
221
|
+
: {}),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return { written: communities.length, summariesKept };
|
|
225
|
+
},
|
|
226
|
+
async listCommunities({ tenantId, limit, cursor, level }) {
|
|
227
|
+
const all = [...graphOf(tenantId).communities.values()]
|
|
228
|
+
.filter((community) => level === undefined || community.level === level)
|
|
229
|
+
.sort((a, b) => a.level - b.level || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
230
|
+
return page(all, limit, cursor);
|
|
231
|
+
},
|
|
232
|
+
async getCommunity({ tenantId, id }) {
|
|
233
|
+
return graphOf(tenantId).communities.get(id) ?? null;
|
|
234
|
+
},
|
|
235
|
+
async setCommunitySummary({ tenantId, id, summary, fingerprint, at }) {
|
|
236
|
+
const graph = graphOf(tenantId);
|
|
237
|
+
const community = graph.communities.get(id);
|
|
238
|
+
if (community === undefined)
|
|
239
|
+
return;
|
|
240
|
+
graph.communities.set(id, { ...community, summary, summaryFingerprint: fingerprint, summarisedAt: at });
|
|
241
|
+
},
|
|
242
|
+
async fingerprint({ tenantId }) {
|
|
243
|
+
const { entities, relationships } = mergeAll(graphOf(tenantId).contributions.values());
|
|
244
|
+
/**
|
|
245
|
+
* A stable serialisation, on the port rather than in a test — AC-6.
|
|
246
|
+
*
|
|
247
|
+
* Every field that a re-index could plausibly reorder is included: surface forms and provenance are the
|
|
248
|
+
* two that actually caught bugs, because both are sets that arrive in whatever order extraction produced.
|
|
249
|
+
* Leaving them out would let the fingerprint agree while the graph differed.
|
|
250
|
+
*/
|
|
251
|
+
const lines = [
|
|
252
|
+
...entities.map((entity) => [
|
|
253
|
+
"E",
|
|
254
|
+
entity.id,
|
|
255
|
+
entity.name,
|
|
256
|
+
entity.type,
|
|
257
|
+
entity.description ?? "",
|
|
258
|
+
entity.surfaceForms.join(","),
|
|
259
|
+
entity.provenance.join(","),
|
|
260
|
+
].join("")),
|
|
261
|
+
...relationships.map((edge) => ["R", edge.id, edge.type, String(edge.weight), edge.description ?? "", edge.provenance.join(",")].join("")),
|
|
262
|
+
];
|
|
263
|
+
return lines.join("\n");
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
//# sourceMappingURL=graph.js.map
|
|
@@ -20,6 +20,7 @@ export * from "./files.js";
|
|
|
20
20
|
export * from "./artifacts.js";
|
|
21
21
|
export * from "./artifact-exports.js";
|
|
22
22
|
export * from "./knowledge.js";
|
|
23
|
+
export * from "./graph.js";
|
|
23
24
|
export * from "./evaluation.js";
|
|
24
25
|
/** Idempotency store reference adapter. */
|
|
25
26
|
export * from "./idempotency.js";
|
|
@@ -36,4 +37,6 @@ export * from "./principal-memory.js";
|
|
|
36
37
|
/** Message + agent reference adapters (used by the embedded agent facade). */
|
|
37
38
|
export * from "./message-store.js";
|
|
38
39
|
export * from "./flows.js";
|
|
40
|
+
export * from "./rate-limit.js";
|
|
41
|
+
export * from "./connections.js";
|
|
39
42
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -88,6 +88,7 @@ export * from "./files.js";
|
|
|
88
88
|
export * from "./artifacts.js";
|
|
89
89
|
export * from "./artifact-exports.js";
|
|
90
90
|
export * from "./knowledge.js";
|
|
91
|
+
export * from "./graph.js";
|
|
91
92
|
export * from "./evaluation.js";
|
|
92
93
|
/** Idempotency store reference adapter. */
|
|
93
94
|
export * from "./idempotency.js";
|
|
@@ -104,4 +105,6 @@ export * from "./principal-memory.js";
|
|
|
104
105
|
/** Message + agent reference adapters (used by the embedded agent facade). */
|
|
105
106
|
export * from "./message-store.js";
|
|
106
107
|
export * from "./flows.js";
|
|
108
|
+
export * from "./rate-limit.js";
|
|
109
|
+
export * from "./connections.js";
|
|
107
110
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rate-limit counter in memory — task #248.
|
|
3
|
+
*
|
|
4
|
+
* **For tests and a single-process deployment only.** Two workers each hold their own `Map`, so each allows the
|
|
5
|
+
* full rate and a limit of 60/minute becomes 60×N. That is not a caveat to be worked around; it is why
|
|
6
|
+
* `createRedisRateLimitStore` exists, and why the cross-process test in this task's suite runs against a real
|
|
7
|
+
* Redis rather than this.
|
|
8
|
+
*
|
|
9
|
+
* Old windows are dropped on write rather than on a timer: a timer in a store would keep a process alive and
|
|
10
|
+
* would need clearing, and the number of live windows is one per tenant.
|
|
11
|
+
*/
|
|
12
|
+
import type { RateLimitStore } from "../../usage/index.js";
|
|
13
|
+
export declare const createMemoryRateLimitStore: () => RateLimitStore;
|
|
14
|
+
//# sourceMappingURL=rate-limit.d.ts.map
|