@retinue/agentkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +310 -0
- package/dist/adapters/bullmq/consumer.d.ts +33 -0
- package/dist/adapters/bullmq/consumer.js +41 -0
- package/dist/adapters/bullmq/dispatcher.d.ts +74 -0
- package/dist/adapters/bullmq/dispatcher.js +160 -0
- package/dist/adapters/bullmq/export.d.ts +31 -0
- package/dist/adapters/bullmq/export.js +53 -0
- package/dist/adapters/bullmq/extraction.d.ts +42 -0
- package/dist/adapters/bullmq/extraction.js +63 -0
- package/dist/adapters/bullmq/index.d.ts +13 -0
- package/dist/adapters/bullmq/index.js +13 -0
- package/dist/adapters/bullmq/lock.d.ts +77 -0
- package/dist/adapters/bullmq/lock.js +126 -0
- package/dist/adapters/bullmq/queue.d.ts +50 -0
- package/dist/adapters/bullmq/queue.js +81 -0
- package/dist/adapters/memory/artifact-exports.d.ts +11 -0
- package/dist/adapters/memory/artifact-exports.js +102 -0
- package/dist/adapters/memory/artifacts.d.ts +15 -0
- package/dist/adapters/memory/artifacts.js +134 -0
- package/dist/adapters/memory/blobs.d.ts +7 -0
- package/dist/adapters/memory/blobs.js +27 -0
- package/dist/adapters/memory/evaluation.d.ts +18 -0
- package/dist/adapters/memory/evaluation.js +148 -0
- package/dist/adapters/memory/files.d.ts +27 -0
- package/dist/adapters/memory/files.js +0 -0
- package/dist/adapters/memory/flows.d.ts +16 -0
- package/dist/adapters/memory/flows.js +117 -0
- package/dist/adapters/memory/hitl.d.ts +9 -0
- package/dist/adapters/memory/hitl.js +130 -0
- package/dist/adapters/memory/idempotency.d.ts +13 -0
- package/dist/adapters/memory/idempotency.js +32 -0
- package/dist/adapters/memory/index.d.ts +39 -0
- package/dist/adapters/memory/index.js +107 -0
- package/dist/adapters/memory/knowledge.d.ts +43 -0
- package/dist/adapters/memory/knowledge.js +248 -0
- package/dist/adapters/memory/mcp.d.ts +9 -0
- package/dist/adapters/memory/mcp.js +37 -0
- package/dist/adapters/memory/message-store.d.ts +17 -0
- package/dist/adapters/memory/message-store.js +70 -0
- package/dist/adapters/memory/principal-memory.d.ts +7 -0
- package/dist/adapters/memory/principal-memory.js +83 -0
- package/dist/adapters/memory/runtime.d.ts +29 -0
- package/dist/adapters/memory/runtime.js +0 -0
- package/dist/adapters/memory/sessions.d.ts +29 -0
- package/dist/adapters/memory/sessions.js +0 -0
- package/dist/adapters/memory/skills.d.ts +10 -0
- package/dist/adapters/memory/skills.js +41 -0
- package/dist/adapters/memory/thread-summary.d.ts +7 -0
- package/dist/adapters/memory/thread-summary.js +29 -0
- package/dist/adapters/memory/usage-limits.d.ts +13 -0
- package/dist/adapters/memory/usage-limits.js +72 -0
- package/dist/adapters/memory/usage.d.ts +16 -0
- package/dist/adapters/memory/usage.js +279 -0
- package/dist/adapters/otel/index.d.ts +111 -0
- package/dist/adapters/otel/index.js +133 -0
- package/dist/adapters/postgres/artifact-exports.d.ts +12 -0
- package/dist/adapters/postgres/artifact-exports.js +117 -0
- package/dist/adapters/postgres/artifacts.d.ts +16 -0
- package/dist/adapters/postgres/artifacts.js +172 -0
- package/dist/adapters/postgres/checkpoint-store.d.ts +16 -0
- package/dist/adapters/postgres/checkpoint-store.js +34 -0
- package/dist/adapters/postgres/config.d.ts +15 -0
- package/dist/adapters/postgres/config.js +187 -0
- package/dist/adapters/postgres/conversation-store.d.ts +4 -0
- package/dist/adapters/postgres/conversation-store.js +82 -0
- package/dist/adapters/postgres/evaluation.d.ts +17 -0
- package/dist/adapters/postgres/evaluation.js +193 -0
- package/dist/adapters/postgres/file-content.d.ts +30 -0
- package/dist/adapters/postgres/file-content.js +111 -0
- package/dist/adapters/postgres/files.d.ts +19 -0
- package/dist/adapters/postgres/files.js +209 -0
- package/dist/adapters/postgres/flows.d.ts +20 -0
- package/dist/adapters/postgres/flows.js +206 -0
- package/dist/adapters/postgres/hitl.d.ts +5 -0
- package/dist/adapters/postgres/hitl.js +247 -0
- package/dist/adapters/postgres/index.d.ts +35 -0
- package/dist/adapters/postgres/index.js +35 -0
- package/dist/adapters/postgres/knowledge.d.ts +48 -0
- package/dist/adapters/postgres/knowledge.js +255 -0
- package/dist/adapters/postgres/memory.d.ts +14 -0
- package/dist/adapters/postgres/memory.js +194 -0
- package/dist/adapters/postgres/message-store.d.ts +11 -0
- package/dist/adapters/postgres/message-store.js +145 -0
- package/dist/adapters/postgres/migrations.d.ts +69 -0
- package/dist/adapters/postgres/migrations.js +1594 -0
- package/dist/adapters/postgres/pg-executor.d.ts +19 -0
- package/dist/adapters/postgres/pg-executor.js +32 -0
- package/dist/adapters/postgres/retention.d.ts +26 -0
- package/dist/adapters/postgres/retention.js +59 -0
- package/dist/adapters/postgres/rollups.d.ts +17 -0
- package/dist/adapters/postgres/rollups.js +267 -0
- package/dist/adapters/postgres/run-coordinator.d.ts +5 -0
- package/dist/adapters/postgres/run-coordinator.js +98 -0
- package/dist/adapters/postgres/run-event-log.d.ts +26 -0
- package/dist/adapters/postgres/run-event-log.js +30 -0
- package/dist/adapters/postgres/run-store.d.ts +4 -0
- package/dist/adapters/postgres/run-store.js +199 -0
- package/dist/adapters/postgres/schema.d.ts +39 -0
- package/dist/adapters/postgres/schema.js +70 -0
- package/dist/adapters/postgres/session-state.d.ts +7 -0
- package/dist/adapters/postgres/session-state.js +99 -0
- package/dist/adapters/postgres/sql.d.ts +8 -0
- package/dist/adapters/postgres/sql.js +2 -0
- package/dist/adapters/postgres/transaction.d.ts +37 -0
- package/dist/adapters/postgres/transaction.js +93 -0
- package/dist/adapters/postgres/unit-of-work.d.ts +18 -0
- package/dist/adapters/postgres/unit-of-work.js +8 -0
- package/dist/adapters/postgres/usage-limits.d.ts +15 -0
- package/dist/adapters/postgres/usage-limits.js +136 -0
- package/dist/adapters/postgres/usage.d.ts +15 -0
- package/dist/adapters/postgres/usage.js +226 -0
- package/dist/adapters/redis/index.d.ts +9 -0
- package/dist/adapters/redis/index.js +9 -0
- package/dist/adapters/redis/realtime.d.ts +74 -0
- package/dist/adapters/redis/realtime.js +112 -0
- package/dist/adapters/supabase/index.d.ts +88 -0
- package/dist/adapters/supabase/index.js +84 -0
- package/dist/adapters/supabase/realtime.d.ts +30 -0
- package/dist/adapters/supabase/realtime.js +53 -0
- package/dist/adapters/supabase/rls.d.ts +99 -0
- package/dist/adapters/supabase/rls.js +216 -0
- package/dist/adapters/supabase/storage.d.ts +50 -0
- package/dist/adapters/supabase/storage.js +207 -0
- package/dist/agents/agent.d.ts +66 -0
- package/dist/agents/agent.js +209 -0
- package/dist/agents/define.d.ts +21 -0
- package/dist/agents/define.js +63 -0
- package/dist/agents/engine.d.ts +98 -0
- package/dist/agents/engine.js +462 -0
- package/dist/agents/index.d.ts +50 -0
- package/dist/agents/index.js +17 -0
- package/dist/artifacts/index.d.ts +114 -0
- package/dist/artifacts/index.js +219 -0
- package/dist/authorization/index.d.ts +76 -0
- package/dist/authorization/index.js +76 -0
- package/dist/capabilities/index.d.ts +120 -0
- package/dist/capabilities/index.js +167 -0
- package/dist/capabilities/runtime.d.ts +89 -0
- package/dist/capabilities/runtime.js +84 -0
- package/dist/citations/index.d.ts +161 -0
- package/dist/citations/index.js +182 -0
- package/dist/context/assembler.d.ts +82 -0
- package/dist/context/assembler.js +129 -0
- package/dist/context/compaction.d.ts +45 -0
- package/dist/context/compaction.js +55 -0
- package/dist/context/index.d.ts +75 -0
- package/dist/context/index.js +17 -0
- package/dist/core/content-parts.d.ts +194 -0
- package/dist/core/content-parts.js +23 -0
- package/dist/core/context.d.ts +51 -0
- package/dist/core/context.js +9 -0
- package/dist/core/env.d.ts +25 -0
- package/dist/core/env.js +41 -0
- package/dist/core/errors.d.ts +30 -0
- package/dist/core/errors.js +65 -0
- package/dist/core/events.d.ts +139 -0
- package/dist/core/events.js +99 -0
- package/dist/core/ids.d.ts +52 -0
- package/dist/core/ids.js +13 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.js +9 -0
- package/dist/core/tokens.d.ts +22 -0
- package/dist/core/tokens.js +22 -0
- package/dist/core/validation.d.ts +34 -0
- package/dist/core/validation.js +176 -0
- package/dist/documents/extraction.d.ts +121 -0
- package/dist/documents/extraction.js +293 -0
- package/dist/documents/index.d.ts +199 -0
- package/dist/documents/index.js +65 -0
- package/dist/documents/parsers/pdf.d.ts +47 -0
- package/dist/documents/parsers/pdf.js +508 -0
- package/dist/documents/parsers/text.d.ts +59 -0
- package/dist/documents/parsers/text.js +325 -0
- package/dist/documents/read-tool.d.ts +52 -0
- package/dist/documents/read-tool.js +109 -0
- package/dist/documents/render.d.ts +29 -0
- package/dist/documents/render.js +59 -0
- package/dist/documents/vision.d.ts +159 -0
- package/dist/documents/vision.js +214 -0
- package/dist/entries/adapters-bullmq.d.ts +8 -0
- package/dist/entries/adapters-bullmq.js +8 -0
- package/dist/entries/adapters-otel.d.ts +13 -0
- package/dist/entries/adapters-otel.js +13 -0
- package/dist/entries/adapters-postgres.d.ts +10 -0
- package/dist/entries/adapters-postgres.js +10 -0
- package/dist/entries/adapters-redis.d.ts +3 -0
- package/dist/entries/adapters-redis.js +3 -0
- package/dist/entries/context.d.ts +20 -0
- package/dist/entries/context.js +20 -0
- package/dist/entries/flows.d.ts +15 -0
- package/dist/entries/flows.js +15 -0
- package/dist/entries/hitl.d.ts +10 -0
- package/dist/entries/hitl.js +10 -0
- package/dist/entries/knowledge.d.ts +18 -0
- package/dist/entries/knowledge.js +19 -0
- package/dist/entries/mcp.d.ts +10 -0
- package/dist/entries/mcp.js +10 -0
- package/dist/entries/observability.d.ts +14 -0
- package/dist/entries/observability.js +16 -0
- package/dist/entries/persistence.d.ts +11 -0
- package/dist/entries/persistence.js +11 -0
- package/dist/entries/providers.d.ts +14 -0
- package/dist/entries/providers.js +14 -0
- package/dist/entries/runtime.d.ts +13 -0
- package/dist/entries/runtime.js +13 -0
- package/dist/entries/server.d.ts +24 -0
- package/dist/entries/server.js +24 -0
- package/dist/entries/tools.d.ts +21 -0
- package/dist/entries/tools.js +21 -0
- package/dist/entries/usage.d.ts +10 -0
- package/dist/entries/usage.js +10 -0
- package/dist/evaluation/gate.d.ts +168 -0
- package/dist/evaluation/gate.js +180 -0
- package/dist/evaluation/graders.d.ts +125 -0
- package/dist/evaluation/graders.js +203 -0
- package/dist/evaluation/index.d.ts +120 -0
- package/dist/evaluation/index.js +183 -0
- package/dist/evaluation/judge.d.ts +75 -0
- package/dist/evaluation/judge.js +111 -0
- package/dist/export/index.d.ts +162 -0
- package/dist/export/index.js +363 -0
- package/dist/export/markdown.d.ts +19 -0
- package/dist/export/markdown.js +29 -0
- package/dist/export/pdf.d.ts +73 -0
- package/dist/export/pdf.js +407 -0
- package/dist/files/context.d.ts +97 -0
- package/dist/files/context.js +185 -0
- package/dist/files/index.d.ts +210 -0
- package/dist/files/index.js +338 -0
- package/dist/files/read-tool.d.ts +81 -0
- package/dist/files/read-tool.js +163 -0
- package/dist/files/turn-parts.d.ts +96 -0
- package/dist/files/turn-parts.js +171 -0
- package/dist/flows/index.d.ts +270 -0
- package/dist/flows/index.js +62 -0
- package/dist/flows/interpreter.d.ts +146 -0
- package/dist/flows/interpreter.js +426 -0
- package/dist/flows/runner.d.ts +145 -0
- package/dist/flows/runner.js +270 -0
- package/dist/graphql/index.d.ts +8 -0
- package/dist/graphql/index.js +8 -0
- package/dist/graphql/resolvers.d.ts +237 -0
- package/dist/graphql/resolvers.js +253 -0
- package/dist/graphql/schema.d.ts +11 -0
- package/dist/graphql/schema.js +258 -0
- package/dist/graphql/sse.d.ts +77 -0
- package/dist/graphql/sse.js +100 -0
- package/dist/hitl/approved-execution.d.ts +127 -0
- package/dist/hitl/approved-execution.js +177 -0
- package/dist/hitl/index.d.ts +79 -0
- package/dist/hitl/index.js +12 -0
- package/dist/hitl/service.d.ts +221 -0
- package/dist/hitl/service.js +268 -0
- package/dist/idempotency/index.d.ts +70 -0
- package/dist/idempotency/index.js +59 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +60 -0
- package/dist/knowledge/chunking.d.ts +57 -0
- package/dist/knowledge/chunking.js +158 -0
- package/dist/knowledge/index.d.ts +119 -0
- package/dist/knowledge/index.js +166 -0
- package/dist/knowledge/retrieval.d.ts +146 -0
- package/dist/knowledge/retrieval.js +170 -0
- package/dist/loadtest/harness.d.ts +168 -0
- package/dist/loadtest/harness.js +507 -0
- package/dist/loadtest/index.d.ts +13 -0
- package/dist/loadtest/index.js +13 -0
- package/dist/loadtest/injection.d.ts +89 -0
- package/dist/loadtest/injection.js +147 -0
- package/dist/loadtest/metrics.d.ts +197 -0
- package/dist/loadtest/metrics.js +160 -0
- package/dist/loadtest/runbooks.d.ts +28 -0
- package/dist/loadtest/runbooks.js +159 -0
- package/dist/loadtest/scenario.d.ts +104 -0
- package/dist/loadtest/scenario.js +208 -0
- package/dist/mcp/egress.d.ts +53 -0
- package/dist/mcp/egress.js +115 -0
- package/dist/mcp/index.d.ts +93 -0
- package/dist/mcp/index.js +33 -0
- package/dist/mcp/provider.d.ts +62 -0
- package/dist/mcp/provider.js +0 -0
- package/dist/models/index.d.ts +98 -0
- package/dist/models/index.js +74 -0
- package/dist/models/pricing.d.ts +24 -0
- package/dist/models/pricing.js +37 -0
- package/dist/models/provider-factory.d.ts +31 -0
- package/dist/models/provider-factory.js +67 -0
- package/dist/models/streaming.d.ts +145 -0
- package/dist/models/streaming.js +272 -0
- package/dist/models/vision.d.ts +38 -0
- package/dist/models/vision.js +62 -0
- package/dist/persistence/index.d.ts +1654 -0
- package/dist/persistence/index.js +226 -0
- package/dist/principal-memory/index.d.ts +106 -0
- package/dist/principal-memory/index.js +89 -0
- package/dist/retention/index.d.ts +89 -0
- package/dist/retention/index.js +70 -0
- package/dist/runtime/checkpoint.d.ts +37 -0
- package/dist/runtime/checkpoint.js +22 -0
- package/dist/runtime/index.d.ts +118 -0
- package/dist/runtime/index.js +69 -0
- package/dist/runtime/retry.d.ts +95 -0
- package/dist/runtime/retry.js +126 -0
- package/dist/runtime/serialization.d.ts +85 -0
- package/dist/runtime/serialization.js +95 -0
- package/dist/runtime/streaming.d.ts +54 -0
- package/dist/runtime/streaming.js +115 -0
- package/dist/runtime/worker.d.ts +130 -0
- package/dist/runtime/worker.js +405 -0
- package/dist/security/checklist.d.ts +53 -0
- package/dist/security/checklist.js +204 -0
- package/dist/security/findings.d.ts +56 -0
- package/dist/security/findings.js +168 -0
- package/dist/security/index.d.ts +14 -0
- package/dist/security/index.js +14 -0
- package/dist/security/prompt-safety.d.ts +100 -0
- package/dist/security/prompt-safety.js +133 -0
- package/dist/server/boot.d.ts +32 -0
- package/dist/server/boot.js +36 -0
- package/dist/server/cli-worker.d.ts +37 -0
- package/dist/server/cli-worker.js +151 -0
- package/dist/server/cli.d.ts +27 -0
- package/dist/server/cli.js +74 -0
- package/dist/server/config.d.ts +42 -0
- package/dist/server/config.js +127 -0
- package/dist/server/health.d.ts +59 -0
- package/dist/server/health.js +90 -0
- package/dist/server/host.d.ts +39 -0
- package/dist/server/host.js +124 -0
- package/dist/server/index.d.ts +15 -0
- package/dist/server/index.js +15 -0
- package/dist/server/main.d.ts +16 -0
- package/dist/server/main.js +31 -0
- package/dist/server/sse-route.d.ts +21 -0
- package/dist/server/sse-route.js +282 -0
- package/dist/skills/index.d.ts +67 -0
- package/dist/skills/index.js +31 -0
- package/dist/skills/resolver.d.ts +54 -0
- package/dist/skills/resolver.js +121 -0
- package/dist/teams/index.d.ts +93 -0
- package/dist/teams/index.js +207 -0
- package/dist/telemetry/index.d.ts +157 -0
- package/dist/telemetry/index.js +71 -0
- package/dist/telemetry/instrument.d.ts +108 -0
- package/dist/telemetry/instrument.js +232 -0
- package/dist/telemetry/log-events.d.ts +17 -0
- package/dist/telemetry/log-events.js +58 -0
- package/dist/telemetry/metrics.d.ts +123 -0
- package/dist/telemetry/metrics.js +135 -0
- package/dist/telemetry/noop.d.ts +39 -0
- package/dist/telemetry/noop.js +143 -0
- package/dist/telemetry/redaction.d.ts +64 -0
- package/dist/telemetry/redaction.js +153 -0
- package/dist/telemetry/spans.d.ts +56 -0
- package/dist/telemetry/spans.js +78 -0
- package/dist/telemetry/trace-context.d.ts +55 -0
- package/dist/telemetry/trace-context.js +60 -0
- package/dist/toolkit/compute.d.ts +53 -0
- package/dist/toolkit/compute.js +152 -0
- package/dist/toolkit/data.d.ts +98 -0
- package/dist/toolkit/data.js +235 -0
- package/dist/toolkit/http.d.ts +113 -0
- package/dist/toolkit/http.js +205 -0
- package/dist/toolkit/index.d.ts +21 -0
- package/dist/toolkit/index.js +17 -0
- package/dist/toolkit/web.d.ts +107 -0
- package/dist/toolkit/web.js +147 -0
- package/dist/tools/define.d.ts +25 -0
- package/dist/tools/define.js +45 -0
- package/dist/tools/delegating.d.ts +132 -0
- package/dist/tools/delegating.js +211 -0
- package/dist/tools/index.d.ts +129 -0
- package/dist/tools/index.js +33 -0
- package/dist/tools/library/compute.d.ts +11 -0
- package/dist/tools/library/compute.js +46 -0
- package/dist/tools/library/data.d.ts +16 -0
- package/dist/tools/library/data.js +92 -0
- package/dist/tools/library/http.d.ts +28 -0
- package/dist/tools/library/http.js +71 -0
- package/dist/tools/library/index.d.ts +97 -0
- package/dist/tools/library/index.js +134 -0
- package/dist/tools/library/knowledge.d.ts +39 -0
- package/dist/tools/library/knowledge.js +58 -0
- package/dist/tools/library/web.d.ts +19 -0
- package/dist/tools/library/web.js +65 -0
- package/dist/tools/meta-tools.d.ts +19 -0
- package/dist/tools/meta-tools.js +36 -0
- package/dist/tools/registry.d.ts +146 -0
- package/dist/tools/registry.js +291 -0
- package/dist/usage/index.d.ts +105 -0
- package/dist/usage/index.js +20 -0
- package/dist/usage/quota.d.ts +258 -0
- package/dist/usage/quota.js +510 -0
- package/dist/usage/recorder.d.ts +29 -0
- package/dist/usage/recorder.js +96 -0
- package/dist/usage/rollups.d.ts +121 -0
- package/dist/usage/rollups.js +157 -0
- package/dist/worker/export.d.ts +57 -0
- package/dist/worker/export.js +81 -0
- package/dist/worker/extraction.d.ts +57 -0
- package/dist/worker/extraction.js +84 -0
- package/dist/worker/main.d.ts +103 -0
- package/dist/worker/main.js +159 -0
- package/package.json +187 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { bucketStartFor, nextBucket } from "../../usage/quota.js";
|
|
2
|
+
import { usageDedupeKey } from "../../usage/index.js";
|
|
3
|
+
const ZERO_TOTALS = {
|
|
4
|
+
inputTokens: 0,
|
|
5
|
+
outputTokens: 0,
|
|
6
|
+
cachedInputTokens: 0,
|
|
7
|
+
reasoningTokens: 0,
|
|
8
|
+
costMinorUnits: 0,
|
|
9
|
+
eventCount: 0,
|
|
10
|
+
};
|
|
11
|
+
// Imported from the port (#100) rather than defined here, so the Postgres adapter cannot drift.
|
|
12
|
+
/**
|
|
13
|
+
* The ledger and its rollups over one set of rows (#139).
|
|
14
|
+
*
|
|
15
|
+
* One backend, because a rollup is *derived* from the ledger and nothing else — deriving it from a second copy
|
|
16
|
+
* of the events would make the two able to disagree, which is the one thing a spend figure must not do.
|
|
17
|
+
*/
|
|
18
|
+
export const createMemoryUsageBackend = (options = {}) => {
|
|
19
|
+
const clock = options.clock ?? (() => new Date().toISOString());
|
|
20
|
+
// tenantId → (dedupeKey → event). Partitioning by tenant makes cross-tenant reads impossible.
|
|
21
|
+
const byTenant = new Map();
|
|
22
|
+
const tenant = (t) => {
|
|
23
|
+
let m = byTenant.get(t);
|
|
24
|
+
if (!m)
|
|
25
|
+
byTenant.set(t, (m = new Map()));
|
|
26
|
+
return m;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* When we learned about each event, keyed the same way as the event.
|
|
30
|
+
*
|
|
31
|
+
* Distinct from `occurredAt`: an event can be recorded late — a delayed provider report, a recovered run
|
|
32
|
+
* replaying its steps — with an `occurredAt` in the past. Staleness judged by `occurredAt` would see its
|
|
33
|
+
* bucket as already computed and never roll it up, which is a silent undercount.
|
|
34
|
+
*
|
|
35
|
+
* A monotonic counter rather than a clock, so two events recorded in the same millisecond still order — and
|
|
36
|
+
* so a test needs no injected clock to observe the property.
|
|
37
|
+
*/
|
|
38
|
+
const recordedAt = new Map();
|
|
39
|
+
let recordSequence = 0;
|
|
40
|
+
const usage = {
|
|
41
|
+
async append({ tenantId, event }) {
|
|
42
|
+
const rows = tenant(tenantId);
|
|
43
|
+
const key = usageDedupeKey(event);
|
|
44
|
+
if (rows.has(key))
|
|
45
|
+
return; // idempotent: a re-recorded step is a no-op
|
|
46
|
+
rows.set(key, event);
|
|
47
|
+
recordedAt.set(`${tenantId} ${key}`, (recordSequence += 1));
|
|
48
|
+
},
|
|
49
|
+
async listByRun({ tenantId, runId, limit, cursor }) {
|
|
50
|
+
const all = [...tenant(tenantId).values()]
|
|
51
|
+
.filter((e) => e.runId === runId)
|
|
52
|
+
.sort((a, b) => (a.occurredAt < b.occurredAt ? -1 : a.occurredAt > b.occurredAt ? 1 : a.id < b.id ? -1 : 1));
|
|
53
|
+
const start = cursor ? all.findIndex((e) => e.id === cursor) + 1 : 0;
|
|
54
|
+
const items = all.slice(start, start + limit);
|
|
55
|
+
const nextCursor = start + limit < all.length ? items[items.length - 1]?.id : undefined;
|
|
56
|
+
const page = nextCursor === undefined ? { items } : { items, nextCursor };
|
|
57
|
+
return page;
|
|
58
|
+
},
|
|
59
|
+
async breakdown({ tenantId, from, to, by, limit }) {
|
|
60
|
+
const scoped = [...tenant(tenantId).values()].filter(
|
|
61
|
+
// Half-open, like every other range in this codebase, so adjacent periods tile without a boundary
|
|
62
|
+
// event appearing in both.
|
|
63
|
+
(e) => e.occurredAt >= from && e.occurredAt < to);
|
|
64
|
+
const groups = new Map();
|
|
65
|
+
for (const e of scoped) {
|
|
66
|
+
// A conversation is optional on a usage event — a background extraction has none — so those group under
|
|
67
|
+
// an explicit key rather than being silently dropped, which would make the breakdown not add up.
|
|
68
|
+
// #175: `principal` alongside the other two. The empty-string fallback is deliberate and matches the
|
|
69
|
+
// SQL adapter — an event with no principal groups under an explicit key rather than being silently
|
|
70
|
+
// dropped, which would make the breakdown not add up.
|
|
71
|
+
const key = by === "model" ? e.modelId : by === "principal" ? (e.principalId ?? "") : (e.conversationId ?? "");
|
|
72
|
+
const current = groups.get(key) ?? ZERO_TOTALS;
|
|
73
|
+
groups.set(key, {
|
|
74
|
+
inputTokens: current.inputTokens + e.inputTokens,
|
|
75
|
+
outputTokens: current.outputTokens + e.outputTokens,
|
|
76
|
+
cachedInputTokens: current.cachedInputTokens + e.cachedInputTokens,
|
|
77
|
+
reasoningTokens: current.reasoningTokens + (e.reasoningTokens ?? 0),
|
|
78
|
+
costMinorUnits: current.costMinorUnits + e.costMinorUnits,
|
|
79
|
+
eventCount: current.eventCount + 1,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return [...groups.entries()]
|
|
83
|
+
.map(([key, totals]) => ({ key, totals }))
|
|
84
|
+
// Largest cost first, so a `limit` drops what matters least; ties by key so the order is stable and a
|
|
85
|
+
// rendered breakdown does not reshuffle between refreshes.
|
|
86
|
+
.sort((a, b) => b.totals.costMinorUnits !== a.totals.costMinorUnits
|
|
87
|
+
? b.totals.costMinorUnits - a.totals.costMinorUnits
|
|
88
|
+
: a.key.localeCompare(b.key))
|
|
89
|
+
.slice(0, limit);
|
|
90
|
+
},
|
|
91
|
+
async totalsBetween({ tenantId, from, to, principalId, modelId }) {
|
|
92
|
+
const scoped = [...tenant(tenantId).values()].filter((e) =>
|
|
93
|
+
// Half-open, like every other range here, so adjacent windows tile without a boundary event counting
|
|
94
|
+
// twice.
|
|
95
|
+
e.occurredAt >= from &&
|
|
96
|
+
e.occurredAt < to &&
|
|
97
|
+
// Absent means the whole tenant. Matching `principalId === undefined` against `e.principalId` would
|
|
98
|
+
// instead select only the records with *no* principal, which is the opposite of "everyone".
|
|
99
|
+
(principalId === undefined || e.principalId === principalId) &&
|
|
100
|
+
(modelId === undefined || e.modelId === modelId));
|
|
101
|
+
const totals = scoped.reduce((acc, e) => ({
|
|
102
|
+
inputTokens: acc.inputTokens + e.inputTokens,
|
|
103
|
+
outputTokens: acc.outputTokens + e.outputTokens,
|
|
104
|
+
cachedInputTokens: acc.cachedInputTokens + e.cachedInputTokens,
|
|
105
|
+
reasoningTokens: acc.reasoningTokens + (e.reasoningTokens ?? 0),
|
|
106
|
+
costMinorUnits: acc.costMinorUnits + e.costMinorUnits,
|
|
107
|
+
eventCount: acc.eventCount + 1,
|
|
108
|
+
}), ZERO_TOTALS);
|
|
109
|
+
// From the same filtered set as the totals, so the two cannot describe different sets of records.
|
|
110
|
+
const earliestAt = scoped.reduce((min, e) => (min === null || e.occurredAt < min ? e.occurredAt : min), null);
|
|
111
|
+
return { totals, earliestAt };
|
|
112
|
+
},
|
|
113
|
+
async totals({ tenantId, runId, conversationId }) {
|
|
114
|
+
const scoped = [...tenant(tenantId).values()].filter((e) => (runId === undefined || e.runId === runId) &&
|
|
115
|
+
(conversationId === undefined || e.conversationId === conversationId));
|
|
116
|
+
return scoped.reduce((acc, e) => ({
|
|
117
|
+
inputTokens: acc.inputTokens + e.inputTokens,
|
|
118
|
+
outputTokens: acc.outputTokens + e.outputTokens,
|
|
119
|
+
cachedInputTokens: acc.cachedInputTokens + e.cachedInputTokens,
|
|
120
|
+
reasoningTokens: acc.reasoningTokens + (e.reasoningTokens ?? 0),
|
|
121
|
+
costMinorUnits: acc.costMinorUnits + e.costMinorUnits,
|
|
122
|
+
eventCount: acc.eventCount + 1,
|
|
123
|
+
}), ZERO_TOTALS);
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
/** Buckets already computed, so `listStaleBuckets` can tell "never computed" from "computed and current". */
|
|
127
|
+
const computed = new Map();
|
|
128
|
+
/** The record sequence a bucket was last computed at, paired with its row. */
|
|
129
|
+
const computedAtSequence = new Map();
|
|
130
|
+
const rollupsFor = (t) => {
|
|
131
|
+
let m = computed.get(t);
|
|
132
|
+
if (!m)
|
|
133
|
+
computed.set(t, (m = new Map()));
|
|
134
|
+
return m;
|
|
135
|
+
};
|
|
136
|
+
/** Events in a bucket, by their `occurredAt`. The only input a rebuild has. */
|
|
137
|
+
const eventsIn = (tenantId, period, bucketStart) => {
|
|
138
|
+
const end = nextBucket(period, bucketStart);
|
|
139
|
+
return [...tenant(tenantId).values()].filter((e) => e.occurredAt >= bucketStart && e.occurredAt < end);
|
|
140
|
+
};
|
|
141
|
+
const sumOf = (events) => events.reduce((acc, e) => ({
|
|
142
|
+
inputTokens: acc.inputTokens + e.inputTokens,
|
|
143
|
+
outputTokens: acc.outputTokens + e.outputTokens,
|
|
144
|
+
cachedInputTokens: acc.cachedInputTokens + e.cachedInputTokens,
|
|
145
|
+
reasoningTokens: acc.reasoningTokens + (e.reasoningTokens ?? 0),
|
|
146
|
+
costMinorUnits: acc.costMinorUnits + e.costMinorUnits,
|
|
147
|
+
eventCount: acc.eventCount + 1,
|
|
148
|
+
}), ZERO_TOTALS);
|
|
149
|
+
/**
|
|
150
|
+
* The rollup map's key, which now carries the grain — #175.
|
|
151
|
+
*
|
|
152
|
+
* `principalId` absent is the **tenant** row; present is one person's. Written as a helper rather than
|
|
153
|
+
* interpolated at five call sites, because the tenant row and a principal's row differing by one segment of a
|
|
154
|
+
* string key is exactly the sort of thing that gets typed slightly differently in one place and reads back
|
|
155
|
+
* empty.
|
|
156
|
+
*/
|
|
157
|
+
const rollupKey = (period, bucketStart, principalId) => `${period} ${bucketStart} ${principalId ?? ""}`;
|
|
158
|
+
const rollups = {
|
|
159
|
+
async rebuild({ tenantId, period, bucketStart, principalId }) {
|
|
160
|
+
const computedAt = clock();
|
|
161
|
+
// A **recomputation**, not an accumulation. Re-running writes the same numbers and two writers racing
|
|
162
|
+
// one bucket write the same value, which is what makes idempotency structural rather than bookkept.
|
|
163
|
+
// The whole bucket, or one person's slice of it. Same computation either way, which is what keeps the
|
|
164
|
+
// two grains from disagreeing.
|
|
165
|
+
const events = eventsIn(tenantId, period, bucketStart).filter((e) => principalId === undefined || e.principalId === principalId);
|
|
166
|
+
const totals = sumOf(events);
|
|
167
|
+
const row = {
|
|
168
|
+
...totals,
|
|
169
|
+
period,
|
|
170
|
+
bucketStart,
|
|
171
|
+
// Carried on the row so `list` and `sum` can filter by grain. Without it a tenant chart would include
|
|
172
|
+
// every principal's row alongside the tenant's and double every figure.
|
|
173
|
+
...(principalId === undefined ? {} : { principalId }),
|
|
174
|
+
// The currency of the events in the bucket. Empty when there are none — a bucket with no spend has no
|
|
175
|
+
// currency, and claiming one would be inventing a fact.
|
|
176
|
+
currency: events[0]?.currency ?? "",
|
|
177
|
+
computedAt,
|
|
178
|
+
};
|
|
179
|
+
rollupsFor(tenantId).set(rollupKey(period, bucketStart, principalId), row);
|
|
180
|
+
// The sequence at the moment of computation, so a *later* append marks the bucket stale again regardless
|
|
181
|
+
// of when the event claims to have occurred.
|
|
182
|
+
computedAtSequence.set(`${tenantId} ${rollupKey(period, bucketStart, principalId)}`, recordSequence);
|
|
183
|
+
return row;
|
|
184
|
+
},
|
|
185
|
+
async get({ tenantId, period, bucketStart, principalId }) {
|
|
186
|
+
// Absent from *this tenant's* map, so a foreign bucket is null without a comparison anyone could get
|
|
187
|
+
// wrong — and no aggregate can span tenants.
|
|
188
|
+
return rollupsFor(tenantId).get(rollupKey(period, bucketStart, principalId)) ?? null;
|
|
189
|
+
},
|
|
190
|
+
async list({ tenantId, period, from, to, limit, cursor, principalId }) {
|
|
191
|
+
const rows = [...rollupsFor(tenantId).values()]
|
|
192
|
+
.filter((r) => r.period === period &&
|
|
193
|
+
r.bucketStart >= from &&
|
|
194
|
+
r.bucketStart < to &&
|
|
195
|
+
// Exactly one grain. `undefined === undefined` selects the tenant rows.
|
|
196
|
+
r.principalId === principalId)
|
|
197
|
+
.sort((a, b) => a.bucketStart.localeCompare(b.bucketStart));
|
|
198
|
+
const start = cursor === undefined ? 0 : rows.findIndex((r) => r.bucketStart > cursor);
|
|
199
|
+
const at = start < 0 ? rows.length : start;
|
|
200
|
+
const items = rows.slice(at, at + limit);
|
|
201
|
+
const last = items[items.length - 1];
|
|
202
|
+
return at + limit < rows.length && last !== undefined
|
|
203
|
+
? { items, nextCursor: last.bucketStart }
|
|
204
|
+
: { items };
|
|
205
|
+
},
|
|
206
|
+
async sum({ tenantId, period, from, to, principalId }) {
|
|
207
|
+
// Over the *rollups*, not the ledger: the whole point of AC-1 is that a read never scans raw records.
|
|
208
|
+
return [...rollupsFor(tenantId).values()]
|
|
209
|
+
.filter((r) => r.period === period &&
|
|
210
|
+
r.bucketStart >= from &&
|
|
211
|
+
r.bucketStart < to &&
|
|
212
|
+
r.principalId === principalId)
|
|
213
|
+
.reduce((acc, r) => ({
|
|
214
|
+
inputTokens: acc.inputTokens + r.inputTokens,
|
|
215
|
+
outputTokens: acc.outputTokens + r.outputTokens,
|
|
216
|
+
cachedInputTokens: acc.cachedInputTokens + r.cachedInputTokens,
|
|
217
|
+
reasoningTokens: acc.reasoningTokens + r.reasoningTokens,
|
|
218
|
+
costMinorUnits: acc.costMinorUnits + r.costMinorUnits,
|
|
219
|
+
eventCount: acc.eventCount + r.eventCount,
|
|
220
|
+
}), ZERO_TOTALS);
|
|
221
|
+
},
|
|
222
|
+
async listStaleBuckets({ tenantId, period, since, limit, cursor }) {
|
|
223
|
+
// Derived from the ledger: a bucket is stale when it holds an event newer than its last computation, or
|
|
224
|
+
// has never been computed. So an interrupted job resumes by asking again -- there is no cursor to lose.
|
|
225
|
+
const rows = rollupsFor(tenantId);
|
|
226
|
+
/**
|
|
227
|
+
* Both grains, in one pass — #175, matching the SQL adapter's GROUPING SETS.
|
|
228
|
+
*
|
|
229
|
+
* A per-principal rollup is only useful if something rebuilds it, and the job cannot know which principals
|
|
230
|
+
* were active in a bucket without reading the ledger. This walk is already reading it, so it reports both:
|
|
231
|
+
* one entry per bucket for the tenant, and one per (bucket, principal).
|
|
232
|
+
*
|
|
233
|
+
* Keyed on the newest *record* sequence, not the newest `occurredAt`, so an event recorded late with a
|
|
234
|
+
* backdated timestamp still marks its bucket stale.
|
|
235
|
+
*/
|
|
236
|
+
const buckets = new Map();
|
|
237
|
+
for (const [key, event] of tenant(tenantId)) {
|
|
238
|
+
if (event.occurredAt < since)
|
|
239
|
+
continue;
|
|
240
|
+
const bucketStart = bucketStartFor(period, event.occurredAt);
|
|
241
|
+
const seq = recordedAt.get(`${tenantId} ${key}`) ?? 0;
|
|
242
|
+
const grains = event.principalId === undefined ? [undefined] : [undefined, event.principalId];
|
|
243
|
+
for (const principalId of grains) {
|
|
244
|
+
const mapKey = rollupKey(period, bucketStart, principalId);
|
|
245
|
+
const seen = buckets.get(mapKey);
|
|
246
|
+
if (seen === undefined || seq > seen.seq)
|
|
247
|
+
buckets.set(mapKey, { bucketStart, ...(principalId === undefined ? {} : { principalId }), seq });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const stale = [...buckets.entries()]
|
|
251
|
+
.filter(([mapKey, entry]) => {
|
|
252
|
+
const existing = rows.get(mapKey);
|
|
253
|
+
if (existing === undefined)
|
|
254
|
+
return true;
|
|
255
|
+
const computedSeq = computedAtSequence.get(`${tenantId} ${mapKey}`) ?? 0;
|
|
256
|
+
return entry.seq > computedSeq;
|
|
257
|
+
})
|
|
258
|
+
.map(([, entry]) => ({
|
|
259
|
+
period,
|
|
260
|
+
bucketStart: entry.bucketStart,
|
|
261
|
+
...(entry.principalId === undefined ? {} : { principalId: entry.principalId }),
|
|
262
|
+
}))
|
|
263
|
+
// The tenant row first within a bucket, so a job that stops mid-bucket has at least the total.
|
|
264
|
+
.sort((a, b) => a.bucketStart.localeCompare(b.bucketStart) ||
|
|
265
|
+
(a.principalId ?? "").localeCompare(b.principalId ?? ""));
|
|
266
|
+
const start = cursor === undefined ? 0 : stale.findIndex((b) => b.bucketStart > cursor);
|
|
267
|
+
const at = start < 0 ? stale.length : start;
|
|
268
|
+
const items = stale.slice(at, at + limit);
|
|
269
|
+
const last = items[items.length - 1];
|
|
270
|
+
return at + limit < stale.length && last !== undefined
|
|
271
|
+
? { items, nextCursor: last.bucketStart }
|
|
272
|
+
: { items };
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
return { usage, rollups };
|
|
276
|
+
};
|
|
277
|
+
export const createMemoryUsageStore = () => createMemoryUsageBackend().usage;
|
|
278
|
+
export const createMemoryUsageRollupStore = () => createMemoryUsageBackend().rollups;
|
|
279
|
+
//# sourceMappingURL=usage.js.map
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OpenTelemetry adapter — AC-6: "instrumentation is vendor-neutral and can target a customer's own
|
|
3
|
+
* collector".
|
|
4
|
+
*
|
|
5
|
+
* The only file in `src/` that knows OpenTelemetry exists, and a boundary rule (R11) keeps it that way. That is
|
|
6
|
+
* what makes the claim true rather than aspirational: a single convenience import of `@opentelemetry/api` in a
|
|
7
|
+
* hot path is how a platform acquires a vendor, and it is invisible in review.
|
|
8
|
+
*
|
|
9
|
+
* **Structural types, no runtime import.** The OTel API surface this needs is declared below as interfaces, and
|
|
10
|
+
* a caller passes their own `TracerProvider` and `MeterProvider`. So `@retinue/agentkit` has no dependency on
|
|
11
|
+
* any OTel package — a customer already running the OTel SDK hands us the objects they have, and a customer
|
|
12
|
+
* running something else implements four small interfaces. `otel.test.ts` imports the *real*
|
|
13
|
+
* `@opentelemetry/api` and passes real providers through, which is the only way to know the structural types are
|
|
14
|
+
* right rather than plausible.
|
|
15
|
+
*
|
|
16
|
+
* That also means "target your own collector" needs nothing from us: exporters, samplers, resource attributes
|
|
17
|
+
* and endpoints are all configured on the provider the caller constructs. We never see a URL.
|
|
18
|
+
*/
|
|
19
|
+
import type { AttributeValue, SpanKind, Telemetry, TelemetryContext } from "../../telemetry/index.js";
|
|
20
|
+
/** OTel's numeric SpanKind enum. Values are fixed by the spec, so the mapping below is stable. */
|
|
21
|
+
export declare const OTEL_SPAN_KIND: Readonly<Record<SpanKind, number>>;
|
|
22
|
+
export interface OtelSpanContext {
|
|
23
|
+
traceId: string;
|
|
24
|
+
spanId: string;
|
|
25
|
+
traceFlags: number;
|
|
26
|
+
}
|
|
27
|
+
export interface OtelSpan {
|
|
28
|
+
spanContext(): OtelSpanContext;
|
|
29
|
+
setAttribute(key: string, value: AttributeValue): unknown;
|
|
30
|
+
setStatus(status: {
|
|
31
|
+
code: number;
|
|
32
|
+
message?: string;
|
|
33
|
+
}): unknown;
|
|
34
|
+
recordException(exception: {
|
|
35
|
+
name?: string;
|
|
36
|
+
message?: string;
|
|
37
|
+
}): unknown;
|
|
38
|
+
end(): unknown;
|
|
39
|
+
}
|
|
40
|
+
export interface OtelTracer {
|
|
41
|
+
startSpan(name: string, options?: {
|
|
42
|
+
kind?: number;
|
|
43
|
+
attributes?: Record<string, AttributeValue>;
|
|
44
|
+
}, context?: unknown): OtelSpan;
|
|
45
|
+
}
|
|
46
|
+
export interface OtelTracerProvider {
|
|
47
|
+
getTracer(name: string, version?: string): OtelTracer;
|
|
48
|
+
}
|
|
49
|
+
export interface OtelCounter {
|
|
50
|
+
add(value: number, attributes?: Record<string, AttributeValue>): unknown;
|
|
51
|
+
}
|
|
52
|
+
export interface OtelHistogram {
|
|
53
|
+
record(value: number, attributes?: Record<string, AttributeValue>): unknown;
|
|
54
|
+
}
|
|
55
|
+
export interface OtelGauge {
|
|
56
|
+
record(value: number, attributes?: Record<string, AttributeValue>): unknown;
|
|
57
|
+
}
|
|
58
|
+
export interface OtelMeter {
|
|
59
|
+
createCounter(name: string, options?: {
|
|
60
|
+
unit?: string;
|
|
61
|
+
description?: string;
|
|
62
|
+
}): OtelCounter;
|
|
63
|
+
createHistogram(name: string, options?: {
|
|
64
|
+
unit?: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
}): OtelHistogram;
|
|
67
|
+
createGauge(name: string, options?: {
|
|
68
|
+
unit?: string;
|
|
69
|
+
description?: string;
|
|
70
|
+
}): OtelGauge;
|
|
71
|
+
}
|
|
72
|
+
export interface OtelMeterProvider {
|
|
73
|
+
getMeter(name: string, version?: string): OtelMeter;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* How a serialized parent becomes an OTel context.
|
|
77
|
+
*
|
|
78
|
+
* Injected, and this is the one seam that genuinely needs it. Building a `Context` containing a remote span
|
|
79
|
+
* requires `trace.setSpanContext(ROOT_CONTEXT, …)` from `@opentelemetry/api` — a *function* from the package we
|
|
80
|
+
* are refusing to import. So the caller supplies it in three lines at wiring time, which is a fair trade for the
|
|
81
|
+
* core staying dependency-free.
|
|
82
|
+
*
|
|
83
|
+
* Without it the adapter still works: spans are created without a remote parent, so a trace stops at the process
|
|
84
|
+
* boundary. Degrading rather than throwing, because a missing three lines of wiring should cost a trace link and
|
|
85
|
+
* not every request.
|
|
86
|
+
*/
|
|
87
|
+
export type RemoteContextFactory = (parent: {
|
|
88
|
+
readonly traceId: string;
|
|
89
|
+
readonly spanId: string;
|
|
90
|
+
readonly traceFlags: number;
|
|
91
|
+
}) => unknown;
|
|
92
|
+
export type OtelTelemetryOptions = {
|
|
93
|
+
readonly tracerProvider: OtelTracerProvider;
|
|
94
|
+
readonly meterProvider: OtelMeterProvider;
|
|
95
|
+
/** Instrumentation scope. Appears on every span and metric, so a collector can attribute them to us. */
|
|
96
|
+
readonly scopeName?: string;
|
|
97
|
+
readonly scopeVersion?: string;
|
|
98
|
+
readonly remoteContext?: RemoteContextFactory;
|
|
99
|
+
/**
|
|
100
|
+
* Where a log line goes.
|
|
101
|
+
*
|
|
102
|
+
* A sink taking a **string**, not a record — the string has already been through `formatLogLine`, which has
|
|
103
|
+
* already been through `redactFields`. A sink that received the record could format it itself and bypass the
|
|
104
|
+
* redaction, and it would be a reasonable-looking thing for someone to write.
|
|
105
|
+
*/
|
|
106
|
+
readonly sink?: (line: string) => void;
|
|
107
|
+
readonly context?: TelemetryContext;
|
|
108
|
+
readonly now?: () => string;
|
|
109
|
+
};
|
|
110
|
+
export declare const createOtelTelemetry: (options: OtelTelemetryOptions) => Telemetry;
|
|
111
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OpenTelemetry adapter — AC-6: "instrumentation is vendor-neutral and can target a customer's own
|
|
3
|
+
* collector".
|
|
4
|
+
*
|
|
5
|
+
* The only file in `src/` that knows OpenTelemetry exists, and a boundary rule (R11) keeps it that way. That is
|
|
6
|
+
* what makes the claim true rather than aspirational: a single convenience import of `@opentelemetry/api` in a
|
|
7
|
+
* hot path is how a platform acquires a vendor, and it is invisible in review.
|
|
8
|
+
*
|
|
9
|
+
* **Structural types, no runtime import.** The OTel API surface this needs is declared below as interfaces, and
|
|
10
|
+
* a caller passes their own `TracerProvider` and `MeterProvider`. So `@retinue/agentkit` has no dependency on
|
|
11
|
+
* any OTel package — a customer already running the OTel SDK hands us the objects they have, and a customer
|
|
12
|
+
* running something else implements four small interfaces. `otel.test.ts` imports the *real*
|
|
13
|
+
* `@opentelemetry/api` and passes real providers through, which is the only way to know the structural types are
|
|
14
|
+
* right rather than plausible.
|
|
15
|
+
*
|
|
16
|
+
* That also means "target your own collector" needs nothing from us: exporters, samplers, resource attributes
|
|
17
|
+
* and endpoints are all configured on the provider the caller constructs. We never see a URL.
|
|
18
|
+
*/
|
|
19
|
+
import { boundMetricAttributes } from "../../telemetry/metrics.js";
|
|
20
|
+
import { formatLogLine, redactFields } from "../../telemetry/redaction.js";
|
|
21
|
+
import { parseTraceparent, TRACE_FLAG_SAMPLED } from "../../telemetry/trace-context.js";
|
|
22
|
+
/* ------------------------------------------------------------------------------------------------------------
|
|
23
|
+
* The OTel API surface, structurally.
|
|
24
|
+
*
|
|
25
|
+
* Narrowed to what is used. A wider declaration would be a copy of someone else's types that drifts, and every
|
|
26
|
+
* member here has to be satisfied by a real provider — which the adapter's test proves against the published
|
|
27
|
+
* package rather than against this file.
|
|
28
|
+
* ---------------------------------------------------------------------------------------------------------- */
|
|
29
|
+
/** OTel's numeric SpanKind enum. Values are fixed by the spec, so the mapping below is stable. */
|
|
30
|
+
export const OTEL_SPAN_KIND = {
|
|
31
|
+
internal: 0,
|
|
32
|
+
server: 1,
|
|
33
|
+
client: 2,
|
|
34
|
+
producer: 3,
|
|
35
|
+
consumer: 4,
|
|
36
|
+
};
|
|
37
|
+
/** OTel's numeric SpanStatusCode. */
|
|
38
|
+
const OTEL_STATUS = { unset: 0, ok: 1, error: 2 };
|
|
39
|
+
const toOtelSpan = (span) => ({
|
|
40
|
+
get context() {
|
|
41
|
+
const ctx = span.spanContext();
|
|
42
|
+
return { traceId: ctx.traceId, spanId: ctx.spanId, traceFlags: ctx.traceFlags };
|
|
43
|
+
},
|
|
44
|
+
setAttributes(attributes) {
|
|
45
|
+
// One at a time: `setAttributes` is on OTel's Span but not on every implementation of it, and `setAttribute`
|
|
46
|
+
// is. Fewer members in the structural type means fewer things a customer's shim has to provide.
|
|
47
|
+
for (const [key, value] of Object.entries(attributes))
|
|
48
|
+
span.setAttribute(key, value);
|
|
49
|
+
},
|
|
50
|
+
recordError({ code, message }) {
|
|
51
|
+
// `name` carries the code. OTel's exception convention wants an Error-ish shape, and this is the honest one:
|
|
52
|
+
// a classified code and an optional message the caller took responsibility for -- never a thrown object,
|
|
53
|
+
// whose stack and cause routinely carry a token or the argument that caused the throw.
|
|
54
|
+
span.recordException({ name: code, ...(message !== undefined ? { message } : {}) });
|
|
55
|
+
},
|
|
56
|
+
setStatus(status) {
|
|
57
|
+
span.setStatus({ code: OTEL_STATUS[status] });
|
|
58
|
+
},
|
|
59
|
+
end() {
|
|
60
|
+
span.end();
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
export const createOtelTelemetry = (options) => {
|
|
64
|
+
const scope = options.scopeName ?? "@retinue/agentkit";
|
|
65
|
+
const otelTracer = options.tracerProvider.getTracer(scope, options.scopeVersion);
|
|
66
|
+
const otelMeter = options.meterProvider.getMeter(scope, options.scopeVersion);
|
|
67
|
+
const sink = options.sink ?? (() => { });
|
|
68
|
+
const now = options.now ?? (() => new Date().toISOString());
|
|
69
|
+
const tracer = {
|
|
70
|
+
startSpan(name, spanOptions = {}) {
|
|
71
|
+
const parent = parseTraceparent(spanOptions.parent);
|
|
72
|
+
const remote = parent !== null && options.remoteContext !== undefined
|
|
73
|
+
? options.remoteContext({
|
|
74
|
+
traceId: parent.traceId,
|
|
75
|
+
spanId: parent.spanId,
|
|
76
|
+
// Default to sampled when the parent's flags say nothing. The alternative -- defaulting to
|
|
77
|
+
// unsampled -- silently drops the child of every span whose producer did not set the bit, which
|
|
78
|
+
// reads as a propagation bug and is very hard to find.
|
|
79
|
+
traceFlags: parent.traceFlags === 0 ? TRACE_FLAG_SAMPLED : parent.traceFlags,
|
|
80
|
+
})
|
|
81
|
+
: undefined;
|
|
82
|
+
return toOtelSpan(otelTracer.startSpan(name, {
|
|
83
|
+
kind: OTEL_SPAN_KIND[spanOptions.kind ?? "internal"],
|
|
84
|
+
...(spanOptions.attributes !== undefined ? { attributes: { ...spanOptions.attributes } } : {}),
|
|
85
|
+
}, remote));
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Metric attributes are bounded here, in the adapter.
|
|
90
|
+
*
|
|
91
|
+
* Not trusted to call sites: `runId` on a latency histogram is one line of code and one time series per run.
|
|
92
|
+
* It looks like helpful detail in review and is a cardinality incident in production, and the bill arrives a
|
|
93
|
+
* month later.
|
|
94
|
+
*/
|
|
95
|
+
const bounded = (attributes) => ({
|
|
96
|
+
...boundMetricAttributes(attributes),
|
|
97
|
+
});
|
|
98
|
+
const meter = {
|
|
99
|
+
counter(name, opts) {
|
|
100
|
+
const counter = otelMeter.createCounter(name, opts);
|
|
101
|
+
return { record: (value, attributes) => void counter.add(value, bounded(attributes)) };
|
|
102
|
+
},
|
|
103
|
+
histogram(name, opts) {
|
|
104
|
+
const histogram = otelMeter.createHistogram(name, opts);
|
|
105
|
+
return { record: (value, attributes) => void histogram.record(value, bounded(attributes)) };
|
|
106
|
+
},
|
|
107
|
+
gauge(name, opts) {
|
|
108
|
+
const gauge = otelMeter.createGauge(name, opts);
|
|
109
|
+
return { record: (value, attributes) => void gauge.record(value, bounded(attributes)) };
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
const makeLogger = (context) => ({
|
|
113
|
+
log(level, event, raw) {
|
|
114
|
+
const { fields, dropped } = redactFields(raw);
|
|
115
|
+
sink(formatLogLine({ level, event, at: now(), context: context, fields }));
|
|
116
|
+
if (dropped.length > 0)
|
|
117
|
+
sink(formatLogLine({
|
|
118
|
+
level: "debug",
|
|
119
|
+
event: "telemetry.fields-dropped",
|
|
120
|
+
at: now(),
|
|
121
|
+
context: context,
|
|
122
|
+
// Names, never values, and capped -- a dropped field's *name* is safe, and eight of them is enough
|
|
123
|
+
// to identify the call site without the notice itself becoming a long line.
|
|
124
|
+
fields: { count: dropped.length, reason: dropped.slice(0, 8).join(",").slice(0, 120) },
|
|
125
|
+
}));
|
|
126
|
+
},
|
|
127
|
+
child(extra) {
|
|
128
|
+
return makeLogger({ ...context, ...extra });
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
return { tracer, meter, logger: makeLogger(options.context ?? { tenantId: "" }) };
|
|
132
|
+
};
|
|
133
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres `ArtifactExportStore` (#134).
|
|
3
|
+
*
|
|
4
|
+
* One property is in the SQL rather than above it: **`claim` is an insert that loses gracefully.**
|
|
5
|
+
* `ON CONFLICT DO NOTHING` against `UNIQUE (tenant_id, artifact_id, version, format)` means two concurrent
|
|
6
|
+
* requests for the same PDF cannot both claim it, whatever the application layer believes — and the loser
|
|
7
|
+
* reads the winner's row rather than erroring, because its next move is the same either way.
|
|
8
|
+
*/
|
|
9
|
+
import type { ArtifactExportStore } from "../../persistence/index.js";
|
|
10
|
+
import type { SqlExecutor } from "./sql.js";
|
|
11
|
+
export declare const createPostgresArtifactExportStore: (sql: SqlExecutor) => ArtifactExportStore;
|
|
12
|
+
//# sourceMappingURL=artifact-exports.d.ts.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres `ArtifactExportStore` (#134).
|
|
3
|
+
*
|
|
4
|
+
* One property is in the SQL rather than above it: **`claim` is an insert that loses gracefully.**
|
|
5
|
+
* `ON CONFLICT DO NOTHING` against `UNIQUE (tenant_id, artifact_id, version, format)` means two concurrent
|
|
6
|
+
* requests for the same PDF cannot both claim it, whatever the application layer believes — and the loser
|
|
7
|
+
* reads the winner's row rather than erroring, because its next move is the same either way.
|
|
8
|
+
*/
|
|
9
|
+
import { asId } from "../../core/ids.js";
|
|
10
|
+
const iso = (v) => (v instanceof Date ? v.toISOString() : v);
|
|
11
|
+
const toExport = (r) => ({
|
|
12
|
+
id: r.id,
|
|
13
|
+
artifactId: asId(r.artifact_id),
|
|
14
|
+
version: Number(r.version),
|
|
15
|
+
format: r.format,
|
|
16
|
+
state: r.state,
|
|
17
|
+
...(r.file_id === null ? {} : { fileId: asId(r.file_id) }),
|
|
18
|
+
// `bigint` comes back as a string from node-postgres, correct past 2^53 and wrong for arithmetic. An
|
|
19
|
+
// export is well inside the safe range, so it is narrowed here rather than left for a caller.
|
|
20
|
+
...(r.byte_size === null ? {} : { byteSize: Number(r.byte_size) }),
|
|
21
|
+
...(r.checksum === null ? {} : { checksum: r.checksum }),
|
|
22
|
+
...(r.failure_reason === null ? {} : { failureReason: r.failure_reason }),
|
|
23
|
+
...(r.failure_message === null ? {} : { failureMessage: r.failure_message }),
|
|
24
|
+
requestedBy: asId(r.requested_by),
|
|
25
|
+
createdAt: iso(r.created_at),
|
|
26
|
+
...(r.rendered_at === null ? {} : { renderedAt: iso(r.rendered_at) }),
|
|
27
|
+
});
|
|
28
|
+
const COLUMNS = `id, artifact_id, version, format, state, file_id, byte_size, checksum,
|
|
29
|
+
failure_reason, failure_message, requested_by, created_at, rendered_at`;
|
|
30
|
+
const encodeCursor = (createdAt, id) => Buffer.from(`${createdAt} ${id}`, "utf8").toString("base64url");
|
|
31
|
+
const decodeCursor = (cursor) => {
|
|
32
|
+
const [createdAt, id] = Buffer.from(cursor, "base64url").toString("utf8").split(" ");
|
|
33
|
+
return createdAt === undefined || id === undefined ? null : { createdAt, id };
|
|
34
|
+
};
|
|
35
|
+
export const createPostgresArtifactExportStore = (sql) => ({
|
|
36
|
+
async claim({ tenantId, export: requested }) {
|
|
37
|
+
const inserted = await sql.query(`INSERT INTO artifact_exports (tenant_id, id, artifact_id, version, format, state,
|
|
38
|
+
requested_by, created_at)
|
|
39
|
+
VALUES ($1,$2,$3,$4,$5,'pending',$6,$7)
|
|
40
|
+
-- The claim. Against the unique constraint, so two concurrent requests for the same export cannot both
|
|
41
|
+
-- proceed to render -- and the loser gets zero rows rather than an error it would have to interpret.
|
|
42
|
+
ON CONFLICT (tenant_id, artifact_id, version, format) DO NOTHING
|
|
43
|
+
RETURNING ${COLUMNS}`, [
|
|
44
|
+
tenantId,
|
|
45
|
+
requested.id,
|
|
46
|
+
requested.artifactId,
|
|
47
|
+
requested.version,
|
|
48
|
+
requested.format,
|
|
49
|
+
requested.requestedBy,
|
|
50
|
+
requested.createdAt,
|
|
51
|
+
]);
|
|
52
|
+
const created = inserted[0];
|
|
53
|
+
if (created !== undefined)
|
|
54
|
+
return { claimed: true, export: toExport(created) };
|
|
55
|
+
const existing = await sql.query(`SELECT ${COLUMNS} FROM artifact_exports
|
|
56
|
+
WHERE tenant_id = $1 AND artifact_id = $2 AND version = $3 AND format = $4`, [tenantId, requested.artifactId, requested.version, requested.format]);
|
|
57
|
+
const row = existing[0];
|
|
58
|
+
if (row === undefined)
|
|
59
|
+
// Deleted between the insert and this read. Treated as a fresh claim rather than thrown, because the
|
|
60
|
+
// caller has nothing better to do and rendering again is correct.
|
|
61
|
+
return { claimed: true, export: { ...requested, state: "pending" } };
|
|
62
|
+
return { claimed: false, export: toExport(row) };
|
|
63
|
+
},
|
|
64
|
+
async complete({ tenantId, id, state, fileId, byteSize, checksum, failureReason, failureMessage, at }) {
|
|
65
|
+
const rows = await sql.query(`UPDATE artifact_exports
|
|
66
|
+
SET state = $3,
|
|
67
|
+
file_id = $4,
|
|
68
|
+
byte_size = $5,
|
|
69
|
+
checksum = $6,
|
|
70
|
+
failure_reason = $7,
|
|
71
|
+
failure_message = $8,
|
|
72
|
+
rendered_at = $9::timestamptz
|
|
73
|
+
-- No compare on the previous state: a worker retrying after a crash does not know what it wrote, and
|
|
74
|
+
-- requiring it to would make recovery impossible.
|
|
75
|
+
WHERE tenant_id = $1 AND id = $2
|
|
76
|
+
RETURNING id`, [
|
|
77
|
+
tenantId,
|
|
78
|
+
id,
|
|
79
|
+
state,
|
|
80
|
+
fileId ?? null,
|
|
81
|
+
byteSize ?? null,
|
|
82
|
+
checksum ?? null,
|
|
83
|
+
failureReason ?? null,
|
|
84
|
+
failureMessage ?? null,
|
|
85
|
+
at,
|
|
86
|
+
]);
|
|
87
|
+
return { recorded: rows.length > 0 };
|
|
88
|
+
},
|
|
89
|
+
async get({ tenantId, id }) {
|
|
90
|
+
const rows = await sql.query(`SELECT ${COLUMNS} FROM artifact_exports WHERE tenant_id = $1 AND id = $2`, [tenantId, id]);
|
|
91
|
+
// A foreign id yields no row, so it is null without a comparison anyone could get wrong.
|
|
92
|
+
return rows[0] === undefined ? null : toExport(rows[0]);
|
|
93
|
+
},
|
|
94
|
+
async find({ tenantId, artifactId, version, format }) {
|
|
95
|
+
const rows = await sql.query(`SELECT ${COLUMNS} FROM artifact_exports
|
|
96
|
+
WHERE tenant_id = $1 AND artifact_id = $2 AND version = $3 AND format = $4`, [tenantId, artifactId, version, format]);
|
|
97
|
+
return rows[0] === undefined ? null : toExport(rows[0]);
|
|
98
|
+
},
|
|
99
|
+
async listByArtifact({ tenantId, artifactId, limit, cursor }) {
|
|
100
|
+
// Keyset on `(created_at, id)`, matching the ORDER BY. An earlier version cursored on `id` alone while
|
|
101
|
+
// ordering by both, which is a cursor that does not describe the sequence it pages -- the page would skip
|
|
102
|
+
// or repeat rows whenever two exports shared a timestamp, which for two formats requested together is the
|
|
103
|
+
// normal case rather than a rare one.
|
|
104
|
+
const after = cursor === undefined ? null : decodeCursor(cursor);
|
|
105
|
+
const rows = await sql.query(`SELECT ${COLUMNS} FROM artifact_exports
|
|
106
|
+
WHERE tenant_id = $1 AND artifact_id = $2
|
|
107
|
+
AND ($3::text IS NULL OR (created_at, id) > ($3::timestamptz, $4::text))
|
|
108
|
+
ORDER BY created_at, id
|
|
109
|
+
LIMIT $5`, [tenantId, artifactId, after?.createdAt ?? null, after?.id ?? null, limit + 1]);
|
|
110
|
+
const items = rows.slice(0, limit).map(toExport);
|
|
111
|
+
const last = items[items.length - 1];
|
|
112
|
+
return rows.length > limit && last !== undefined
|
|
113
|
+
? { items, nextCursor: encodeCursor(last.createdAt, last.id) }
|
|
114
|
+
: { items };
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
//# sourceMappingURL=artifact-exports.js.map
|