@retinue/agentkit 0.2.0 → 0.3.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/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 +19 -0
- package/dist/server/bin.js +226 -0
- package/dist/server/boot.d.ts +20 -2
- package/dist/server/boot.js +29 -3
- package/dist/server/cli.d.ts +13 -0
- package/dist/server/cli.js +7 -4
- package/dist/server/doctor.d.ts +74 -0
- package/dist/server/doctor.js +280 -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
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `retinue doctor` — task #252 AC-4.
|
|
3
|
+
*
|
|
4
|
+
* The one command here that is new rather than a wrapper, and the one that pays for itself: every failure it
|
|
5
|
+
* names is otherwise a support conversation. A deployment that will not start currently produces one error, from
|
|
6
|
+
* whichever check happened to run first, and the operator fixes it and runs again to find the next.
|
|
7
|
+
*
|
|
8
|
+
* ## Every failure, not the first
|
|
9
|
+
*
|
|
10
|
+
* The whole value is in the plural. A doctor that stops at the first problem sends the user round the loop once
|
|
11
|
+
* per problem — which is exactly what starting the server already does, so it would add nothing. Each check is
|
|
12
|
+
* therefore independent and its own failure is caught, so a Postgres that will not connect does not prevent the
|
|
13
|
+
* Redis check from running.
|
|
14
|
+
*
|
|
15
|
+
* ## Nothing here prints a secret
|
|
16
|
+
*
|
|
17
|
+
* A diagnostic tool is exactly where a URL with a password in it gets pasted into a support ticket. So a
|
|
18
|
+
* connection string is reported by its *shape* — host and database, never userinfo — and a failure message is
|
|
19
|
+
* the driver's, scrubbed. `mcp/egress.ts` already refuses credentials in a URL for the same reason.
|
|
20
|
+
*/
|
|
21
|
+
export type CheckResult = {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly ok: boolean;
|
|
24
|
+
/** What was found. Never a secret. */
|
|
25
|
+
readonly detail: string;
|
|
26
|
+
/** Present when `ok` is false: what to do about it. */
|
|
27
|
+
readonly remedy?: string;
|
|
28
|
+
/**
|
|
29
|
+
* A check that could not run rather than one that failed.
|
|
30
|
+
*
|
|
31
|
+
* Distinct because the two need different responses: a failed check is a broken deployment, and a skipped one
|
|
32
|
+
* is a deployment this tool cannot see into. Reporting a skip as a pass would be the "passes having checked
|
|
33
|
+
* nothing" failure; reporting it as a failure would tell an operator to fix something that is not wrong.
|
|
34
|
+
*/
|
|
35
|
+
readonly skipped?: boolean;
|
|
36
|
+
};
|
|
37
|
+
export declare const describeUrl: (value: string) => string;
|
|
38
|
+
/** The driver's message, with anything URL-shaped reduced. Errors quote the connection string routinely. */
|
|
39
|
+
export declare const scrub: (message: string) => string;
|
|
40
|
+
export type DoctorDeps = {
|
|
41
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
42
|
+
/** Injected so the checks are testable without a database or a Redis. */
|
|
43
|
+
readonly connectPostgres?: (url: string) => Promise<{
|
|
44
|
+
query(text: string, params?: readonly unknown[]): Promise<unknown>;
|
|
45
|
+
end(): Promise<void>;
|
|
46
|
+
}>;
|
|
47
|
+
readonly connectRedis?: (url: string) => Promise<{
|
|
48
|
+
ping(): Promise<string>;
|
|
49
|
+
quit(): Promise<unknown>;
|
|
50
|
+
}>;
|
|
51
|
+
readonly schemaVersions?: (sql: {
|
|
52
|
+
query(text: string, params?: readonly unknown[]): Promise<unknown>;
|
|
53
|
+
}) => Promise<{
|
|
54
|
+
current: number;
|
|
55
|
+
target: number;
|
|
56
|
+
}>;
|
|
57
|
+
};
|
|
58
|
+
/** How long any one check may take before it is reported as a timeout. */
|
|
59
|
+
export declare const CHECK_TIMEOUT_MS = 5000;
|
|
60
|
+
/**
|
|
61
|
+
* A backstop, because **a diagnostic that hangs is worse than one that fails.**
|
|
62
|
+
*
|
|
63
|
+
* Found by running `doctor` against a refused port: `pg.Pool` has no default connect timeout and `ioredis`
|
|
64
|
+
* retries a refused connection for ever, so the command sat there silently instead of reporting the two
|
|
65
|
+
* failures it existed to report. Both drivers are now configured to fail fast, and this is the belt to that
|
|
66
|
+
* braces — a driver whose timeout option is wrong or renamed must not be able to hang the tool.
|
|
67
|
+
*
|
|
68
|
+
* The timer is unref'd so a completed check never holds the process open past its work.
|
|
69
|
+
*/
|
|
70
|
+
export declare const withTimeout: <T>(what: string, work: Promise<T>, ms?: number) => Promise<T>;
|
|
71
|
+
export declare const runChecks: (deps?: DoctorDeps) => Promise<readonly CheckResult[]>;
|
|
72
|
+
/** Human output. Returns the exit code, so the caller does not decide what a failure means. */
|
|
73
|
+
export declare const report: (results: readonly CheckResult[], write?: (line: string) => void) => number;
|
|
74
|
+
//# sourceMappingURL=doctor.d.ts.map
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `retinue doctor` — task #252 AC-4.
|
|
3
|
+
*
|
|
4
|
+
* The one command here that is new rather than a wrapper, and the one that pays for itself: every failure it
|
|
5
|
+
* names is otherwise a support conversation. A deployment that will not start currently produces one error, from
|
|
6
|
+
* whichever check happened to run first, and the operator fixes it and runs again to find the next.
|
|
7
|
+
*
|
|
8
|
+
* ## Every failure, not the first
|
|
9
|
+
*
|
|
10
|
+
* The whole value is in the plural. A doctor that stops at the first problem sends the user round the loop once
|
|
11
|
+
* per problem — which is exactly what starting the server already does, so it would add nothing. Each check is
|
|
12
|
+
* therefore independent and its own failure is caught, so a Postgres that will not connect does not prevent the
|
|
13
|
+
* Redis check from running.
|
|
14
|
+
*
|
|
15
|
+
* ## Nothing here prints a secret
|
|
16
|
+
*
|
|
17
|
+
* A diagnostic tool is exactly where a URL with a password in it gets pasted into a support ticket. So a
|
|
18
|
+
* connection string is reported by its *shape* — host and database, never userinfo — and a failure message is
|
|
19
|
+
* the driver's, scrubbed. `mcp/egress.ts` already refuses credentials in a URL for the same reason.
|
|
20
|
+
*/
|
|
21
|
+
import { ConfigurationError, loadConfig } from "./config.js";
|
|
22
|
+
/**
|
|
23
|
+
* A connection string reduced to what is safe to print.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately not a regex over the whole string: `URL` parsing means a password containing a `@` or a `/`
|
|
26
|
+
* cannot smuggle itself into the output. An unparseable value is reported as unparseable rather than echoed,
|
|
27
|
+
* because echoing it is how a secret reaches a log.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* A connection string reduced to what is safe to print, by **whitelist**.
|
|
31
|
+
*
|
|
32
|
+
* Two earlier versions of this leaked, and both leaked the same way — by trying to *remove* the secret from a
|
|
33
|
+
* string whose structure was not what they assumed:
|
|
34
|
+
*
|
|
35
|
+
* 1. Parsing with `URL` and printing `hostname`/`port`/`pathname`. For
|
|
36
|
+
* `postgres://user:p@ss/word@db.internal:5432/app`, `URL` reads `user:p` as the userinfo and `ss` as the
|
|
37
|
+
* hostname, so the "safe" output was `postgres://ss/word@db.internal:5432/app` — half the password, spread
|
|
38
|
+
* across the host and path.
|
|
39
|
+
* 2. Refusing when the *authority* contained two `@`. The authority ends at the first `/`, and in that example
|
|
40
|
+
* the second `@` is after it. So the check passed and the leak stood.
|
|
41
|
+
*
|
|
42
|
+
* A redaction function must therefore not try to find the secret. It matches a strict, unambiguous shape and
|
|
43
|
+
* prints only the groups it captured; anything else is refused unprinted. An unescaped `@` or `/` in a password
|
|
44
|
+
* is common, and it is exactly what makes a connection string ambiguous — so ambiguity is the thing to refuse,
|
|
45
|
+
* not to parse harder.
|
|
46
|
+
*/
|
|
47
|
+
const SAFE_URL = /^([a-z][a-z0-9+.-]*):\/\/(?:[^@/?#]*@)?([A-Za-z0-9._-]+|\[[0-9A-Fa-f:]+\])(?::(\d+))?(?:\/([A-Za-z0-9._-]*))?$/;
|
|
48
|
+
export const describeUrl = (value) => {
|
|
49
|
+
const match = SAFE_URL.exec(value);
|
|
50
|
+
if (match === null)
|
|
51
|
+
return "(not printed — the value does not match an unambiguous connection-string shape)";
|
|
52
|
+
const [, scheme, host, port, database] = match;
|
|
53
|
+
return `${scheme}://${host}${port === undefined ? "" : `:${port}`}${database ? `/${database}` : ""}`;
|
|
54
|
+
};
|
|
55
|
+
/** The driver's message, with anything URL-shaped reduced. Errors quote the connection string routinely. */
|
|
56
|
+
export const scrub = (message) => message.replace(/\b[a-z][a-z0-9+.-]*:\/\/[^\s'"]+/gi, (m) => describeUrl(m));
|
|
57
|
+
const message = (error) => scrub(error instanceof Error ? error.message : String(error));
|
|
58
|
+
/** True when `describeUrl` refused to print a value, so nothing derived from it can be trusted either. */
|
|
59
|
+
const unsafe = (described) => described.startsWith("(");
|
|
60
|
+
/**
|
|
61
|
+
* What to say about a connection failure — the URL and the driver's message, or neither.
|
|
62
|
+
*
|
|
63
|
+
* A driver interpolates whatever *it* parsed, so `postgres://u:p@ss/word@host/db` fails with
|
|
64
|
+
* `getaddrinfo ENOTFOUND ss` — a fragment of the password, in a message no scrubber can recognise as one
|
|
65
|
+
* because `ss` is not URL-shaped. The rule that follows is simple and the only safe one: if the value could not
|
|
66
|
+
* be printed, the error derived from it is not printed either. The remedy still names the variable, which is
|
|
67
|
+
* what the operator needs.
|
|
68
|
+
*/
|
|
69
|
+
const failureDetail = (url, error) => {
|
|
70
|
+
const described = describeUrl(url);
|
|
71
|
+
return unsafe(described)
|
|
72
|
+
? `${described} The driver's message is withheld for the same reason — it can quote the value it parsed.`
|
|
73
|
+
: `${described}: ${message(error)}`;
|
|
74
|
+
};
|
|
75
|
+
/** How long any one check may take before it is reported as a timeout. */
|
|
76
|
+
export const CHECK_TIMEOUT_MS = 5_000;
|
|
77
|
+
/**
|
|
78
|
+
* A backstop, because **a diagnostic that hangs is worse than one that fails.**
|
|
79
|
+
*
|
|
80
|
+
* Found by running `doctor` against a refused port: `pg.Pool` has no default connect timeout and `ioredis`
|
|
81
|
+
* retries a refused connection for ever, so the command sat there silently instead of reporting the two
|
|
82
|
+
* failures it existed to report. Both drivers are now configured to fail fast, and this is the belt to that
|
|
83
|
+
* braces — a driver whose timeout option is wrong or renamed must not be able to hang the tool.
|
|
84
|
+
*
|
|
85
|
+
* The timer is unref'd so a completed check never holds the process open past its work.
|
|
86
|
+
*/
|
|
87
|
+
export const withTimeout = async (what, work, ms = CHECK_TIMEOUT_MS) => {
|
|
88
|
+
let timer;
|
|
89
|
+
try {
|
|
90
|
+
return await Promise.race([
|
|
91
|
+
work,
|
|
92
|
+
new Promise((_, reject) => {
|
|
93
|
+
timer = setTimeout(() => reject(new Error(`${what} did not respond within ${ms}ms`)), ms);
|
|
94
|
+
timer.unref?.();
|
|
95
|
+
}),
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
if (timer !== undefined)
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
export const runChecks = async (deps = {}) => {
|
|
104
|
+
const env = deps.env ?? process.env;
|
|
105
|
+
const results = [];
|
|
106
|
+
// 1. Configuration. Everything else needs it, so a failure here skips the rest rather than reporting
|
|
107
|
+
// cascading failures that are all the same problem.
|
|
108
|
+
let config;
|
|
109
|
+
try {
|
|
110
|
+
config = loadConfig(env);
|
|
111
|
+
results.push({ name: "configuration", ok: true, detail: `schema mode ${config.schemaMode}, port ${config.port}` });
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
results.push({
|
|
115
|
+
name: "configuration",
|
|
116
|
+
ok: false,
|
|
117
|
+
detail: message(error),
|
|
118
|
+
remedy: error instanceof ConfigurationError
|
|
119
|
+
? `Set or correct: ${error.variables.join(", ")}. See .env.example.`
|
|
120
|
+
: "See .env.example for the variables this deployment needs.",
|
|
121
|
+
});
|
|
122
|
+
for (const name of ["postgres", "schema", "redis"]) {
|
|
123
|
+
results.push({
|
|
124
|
+
name,
|
|
125
|
+
ok: true,
|
|
126
|
+
skipped: true,
|
|
127
|
+
detail: "not checked — configuration is unusable, so this would fail for the same reason",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return results;
|
|
131
|
+
}
|
|
132
|
+
// 2. The app module, which `serve` and `worker` need and `migrate` does not.
|
|
133
|
+
const appModule = env["RETINUE_APP_MODULE"];
|
|
134
|
+
if (appModule === undefined || appModule.trim() === "") {
|
|
135
|
+
results.push({
|
|
136
|
+
name: "app module",
|
|
137
|
+
ok: true,
|
|
138
|
+
skipped: true,
|
|
139
|
+
detail: "RETINUE_APP_MODULE is unset — `migrate` and `doctor` work without it; `serve` and `worker` do not",
|
|
140
|
+
remedy: "Point RETINUE_APP_MODULE at a module default-exporting { authenticate, deps } before serving.",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
try {
|
|
145
|
+
const loaded = (await import(appModule));
|
|
146
|
+
const app = loaded.default;
|
|
147
|
+
const ok = typeof app?.authenticate === "function" && typeof app?.deps === "function";
|
|
148
|
+
results.push({
|
|
149
|
+
name: "app module",
|
|
150
|
+
ok,
|
|
151
|
+
detail: ok ? `${appModule} exports { authenticate, deps }` : `${appModule} loaded but is missing exports`,
|
|
152
|
+
...(ok ? {} : { remedy: "It must default-export { authenticate, deps }. `authenticate` has no default." }),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
results.push({
|
|
157
|
+
name: "app module",
|
|
158
|
+
ok: false,
|
|
159
|
+
detail: `${appModule} could not be loaded: ${message(error)}`,
|
|
160
|
+
remedy: "Check the path is resolvable from the working directory, and that the module builds.",
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// 3. Postgres, and 4. the schema — one connection, two findings.
|
|
165
|
+
const connectPostgres = deps.connectPostgres;
|
|
166
|
+
if (connectPostgres === undefined) {
|
|
167
|
+
results.push({ name: "postgres", ok: true, skipped: true, detail: "no driver supplied" });
|
|
168
|
+
results.push({ name: "schema", ok: true, skipped: true, detail: "no driver supplied" });
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
let sql;
|
|
172
|
+
/**
|
|
173
|
+
* Whether Postgres *answered*, which is not the same as whether a pool object exists.
|
|
174
|
+
*
|
|
175
|
+
* `new Pool()` does not connect, so `sql` is defined even when the database is unreachable — and keying the
|
|
176
|
+
* schema check on `sql !== undefined` reported "0 of 30 migrations applied → Run `retinue migrate`" against
|
|
177
|
+
* a database nobody could reach. That is a diagnostic sending an operator to fix the wrong thing, which is
|
|
178
|
+
* worse than reporting nothing.
|
|
179
|
+
*/
|
|
180
|
+
let reachable = false;
|
|
181
|
+
try {
|
|
182
|
+
sql = await withTimeout("postgres", connectPostgres(config.databaseUrl));
|
|
183
|
+
await withTimeout("postgres", sql.query("select 1"));
|
|
184
|
+
reachable = true;
|
|
185
|
+
results.push({ name: "postgres", ok: true, detail: `reachable at ${describeUrl(config.databaseUrl)}` });
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
results.push({
|
|
189
|
+
name: "postgres",
|
|
190
|
+
ok: false,
|
|
191
|
+
detail: failureDetail(config.databaseUrl, error),
|
|
192
|
+
remedy: "Check the database is running and RETINUE_DATABASE_URL points at it.",
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (reachable && sql !== undefined && deps.schemaVersions !== undefined) {
|
|
196
|
+
try {
|
|
197
|
+
const { current, target } = await withTimeout("schema", deps.schemaVersions(sql));
|
|
198
|
+
// Ahead is reported as a failure, not a pass: a database migrated by a newer build than this one is a
|
|
199
|
+
// deployment about to behave unpredictably, and "current >= target" would call it healthy.
|
|
200
|
+
const ok = current === target;
|
|
201
|
+
results.push({
|
|
202
|
+
name: "schema",
|
|
203
|
+
ok,
|
|
204
|
+
detail: `${current} of ${target} migrations applied`,
|
|
205
|
+
...(ok
|
|
206
|
+
? {}
|
|
207
|
+
: {
|
|
208
|
+
remedy: current < target
|
|
209
|
+
? "Run `retinue migrate`."
|
|
210
|
+
: "This database is ahead of this build. Deploy the matching version rather than migrating down.",
|
|
211
|
+
}),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
results.push({
|
|
216
|
+
name: "schema",
|
|
217
|
+
ok: false,
|
|
218
|
+
detail: message(error),
|
|
219
|
+
remedy: "Run `retinue migrate --status` for detail.",
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else if (reachable) {
|
|
224
|
+
results.push({ name: "schema", ok: true, skipped: true, detail: "no schema reader supplied" });
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
results.push({
|
|
228
|
+
name: "schema",
|
|
229
|
+
ok: true,
|
|
230
|
+
skipped: true,
|
|
231
|
+
detail: "not checked — Postgres is unreachable, so this would fail for the same reason",
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (sql !== undefined)
|
|
235
|
+
await sql.end().catch(() => undefined);
|
|
236
|
+
}
|
|
237
|
+
// 5. Redis.
|
|
238
|
+
if (deps.connectRedis === undefined) {
|
|
239
|
+
results.push({ name: "redis", ok: true, skipped: true, detail: "no client supplied" });
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
let redis;
|
|
243
|
+
try {
|
|
244
|
+
redis = await withTimeout("redis", deps.connectRedis(config.redisUrl));
|
|
245
|
+
const pong = await withTimeout("redis", redis.ping());
|
|
246
|
+
results.push({ name: "redis", ok: true, detail: `reachable at ${describeUrl(config.redisUrl)} (${pong})` });
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
results.push({
|
|
250
|
+
name: "redis",
|
|
251
|
+
ok: false,
|
|
252
|
+
detail: failureDetail(config.redisUrl, error),
|
|
253
|
+
remedy: "Check Redis is running and RETINUE_REDIS_URL points at it.",
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
// Always, including after a failure. Leaving a client open kept the socket handle alive and the command
|
|
258
|
+
// never exited — a diagnostic that reports correctly and then hangs is still a diagnostic that hangs.
|
|
259
|
+
if (redis !== undefined)
|
|
260
|
+
await redis.quit().catch(() => undefined);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return results;
|
|
264
|
+
};
|
|
265
|
+
/** Human output. Returns the exit code, so the caller does not decide what a failure means. */
|
|
266
|
+
export const report = (results, write = console.log) => {
|
|
267
|
+
const failed = results.filter((r) => !r.ok);
|
|
268
|
+
for (const r of results) {
|
|
269
|
+
const mark = !r.ok ? "✗" : r.skipped === true ? "–" : "✓";
|
|
270
|
+
write(`${mark} ${r.name}: ${r.detail}`);
|
|
271
|
+
if (r.remedy !== undefined)
|
|
272
|
+
write(` → ${r.remedy}`);
|
|
273
|
+
}
|
|
274
|
+
write("");
|
|
275
|
+
write(failed.length === 0
|
|
276
|
+
? `✓ ${results.filter((r) => r.skipped !== true).length} check(s) passed`
|
|
277
|
+
: `✗ ${failed.length} of ${results.length} check(s) failed`);
|
|
278
|
+
return failed.length === 0 ? 0 : 1;
|
|
279
|
+
};
|
|
280
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills in context — task #244, the interpreter for `AgentManifest.skillPolicy`.
|
|
3
|
+
*
|
|
4
|
+
* `skillPolicy` was declared and read by nothing. That was not one missing line: the *subsystem* was complete and
|
|
5
|
+
* unreachable. `SkillResolver.listCatalog` already takes `{ tenantId, assigned, allowTenantSkills }` — the
|
|
6
|
+
* manifest's two fields verbatim — the store had memory and Postgres adapters under a conformance suite, and
|
|
7
|
+
* `ContextKind`/`ContextBudget` already reserved a `skills` bucket for a section nothing produced. Two things
|
|
8
|
+
* were missing: something to emit that section, and an implementation of `load_skill` (see
|
|
9
|
+
* `tools/registry.ts`).
|
|
10
|
+
*
|
|
11
|
+
* ## Why the catalogue and not the bodies
|
|
12
|
+
*
|
|
13
|
+
* A skill body is up to 20,000 characters (`SKILL_LIMITS.instructionsMaxLength`). Twenty-five of those is a
|
|
14
|
+
* context window. So the section lists **names and descriptions**, and the model calls `load_skill` for the one
|
|
15
|
+
* it wants — the same two-tier shape the tool catalogue uses, for the same reason.
|
|
16
|
+
*
|
|
17
|
+
* ## Origin is `platform`, and the interpolated values are neutralised
|
|
18
|
+
*
|
|
19
|
+
* A skill body may instruct the agent — that is what a skill *is* — so this section is `platform` rather than
|
|
20
|
+
* `external`. But a *tenant-authored* skill's name and description are written by a customer, and this section
|
|
21
|
+
* carries them. Wrapping the whole section in an untrusted envelope would say "nothing in here is an
|
|
22
|
+
* instruction", which is false and would break skills; so the section stays `platform` and neutralises the
|
|
23
|
+
* values it interpolates, which is exactly the case `ContextSection.origin`'s own documentation describes.
|
|
24
|
+
*/
|
|
25
|
+
import type { ContextProvider } from "../context/index.js";
|
|
26
|
+
import { type SkillResolver } from "./index.js";
|
|
27
|
+
import type { SkillBodyLoader } from "../tools/registry.js";
|
|
28
|
+
/** The provider id a manifest names in `contextProviderIds`. */
|
|
29
|
+
export declare const SKILL_CATALOGUE_PROVIDER_ID = "skill-catalogue";
|
|
30
|
+
/**
|
|
31
|
+
* Strips what could end a delimited block or forge a heading in the rendered prompt.
|
|
32
|
+
*
|
|
33
|
+
* Narrow on purpose: a skill description is prose a customer wrote, and mangling it would make the catalogue
|
|
34
|
+
* unreadable. Newlines go because the section is one line per skill and a description containing a line break
|
|
35
|
+
* could otherwise invent a row; backticks and the sequences that open a fenced block go because the surrounding
|
|
36
|
+
* render uses them structurally.
|
|
37
|
+
*/
|
|
38
|
+
export declare const neutralise: (value: string) => string;
|
|
39
|
+
export declare const createSkillCatalogueProvider: (deps: {
|
|
40
|
+
readonly resolver: SkillResolver;
|
|
41
|
+
readonly policy: {
|
|
42
|
+
readonly assigned: readonly string[];
|
|
43
|
+
readonly allowTenantSkills: boolean;
|
|
44
|
+
};
|
|
45
|
+
}) => ContextProvider;
|
|
46
|
+
/**
|
|
47
|
+
* Adapts a `SkillResolver` to the registry's structural `SkillBodyLoader`.
|
|
48
|
+
*
|
|
49
|
+
* The version is not a parameter. `loadBody` pins to an exact version so a mid-run edit cannot change behaviour,
|
|
50
|
+
* and the version the model should get is the one the catalogue it just read advertised — so it is looked up
|
|
51
|
+
* here rather than trusted from the model's arguments. A model naming a version would be a model choosing which
|
|
52
|
+
* revision of an instruction to follow.
|
|
53
|
+
*/
|
|
54
|
+
export declare const createSkillBodyLoader: (deps: {
|
|
55
|
+
readonly resolver: SkillResolver;
|
|
56
|
+
readonly policy: {
|
|
57
|
+
readonly assigned: readonly string[];
|
|
58
|
+
readonly allowTenantSkills: boolean;
|
|
59
|
+
};
|
|
60
|
+
}) => SkillBodyLoader;
|
|
61
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills in context — task #244, the interpreter for `AgentManifest.skillPolicy`.
|
|
3
|
+
*
|
|
4
|
+
* `skillPolicy` was declared and read by nothing. That was not one missing line: the *subsystem* was complete and
|
|
5
|
+
* unreachable. `SkillResolver.listCatalog` already takes `{ tenantId, assigned, allowTenantSkills }` — the
|
|
6
|
+
* manifest's two fields verbatim — the store had memory and Postgres adapters under a conformance suite, and
|
|
7
|
+
* `ContextKind`/`ContextBudget` already reserved a `skills` bucket for a section nothing produced. Two things
|
|
8
|
+
* were missing: something to emit that section, and an implementation of `load_skill` (see
|
|
9
|
+
* `tools/registry.ts`).
|
|
10
|
+
*
|
|
11
|
+
* ## Why the catalogue and not the bodies
|
|
12
|
+
*
|
|
13
|
+
* A skill body is up to 20,000 characters (`SKILL_LIMITS.instructionsMaxLength`). Twenty-five of those is a
|
|
14
|
+
* context window. So the section lists **names and descriptions**, and the model calls `load_skill` for the one
|
|
15
|
+
* it wants — the same two-tier shape the tool catalogue uses, for the same reason.
|
|
16
|
+
*
|
|
17
|
+
* ## Origin is `platform`, and the interpolated values are neutralised
|
|
18
|
+
*
|
|
19
|
+
* A skill body may instruct the agent — that is what a skill *is* — so this section is `platform` rather than
|
|
20
|
+
* `external`. But a *tenant-authored* skill's name and description are written by a customer, and this section
|
|
21
|
+
* carries them. Wrapping the whole section in an untrusted envelope would say "nothing in here is an
|
|
22
|
+
* instruction", which is false and would break skills; so the section stays `platform` and neutralises the
|
|
23
|
+
* values it interpolates, which is exactly the case `ContextSection.origin`'s own documentation describes.
|
|
24
|
+
*/
|
|
25
|
+
import { estimateTokens } from "../core/tokens.js";
|
|
26
|
+
import { SKILL_LIMITS } from "./index.js";
|
|
27
|
+
/** The provider id a manifest names in `contextProviderIds`. */
|
|
28
|
+
export const SKILL_CATALOGUE_PROVIDER_ID = "skill-catalogue";
|
|
29
|
+
/**
|
|
30
|
+
* Strips what could end a delimited block or forge a heading in the rendered prompt.
|
|
31
|
+
*
|
|
32
|
+
* Narrow on purpose: a skill description is prose a customer wrote, and mangling it would make the catalogue
|
|
33
|
+
* unreadable. Newlines go because the section is one line per skill and a description containing a line break
|
|
34
|
+
* could otherwise invent a row; backticks and the sequences that open a fenced block go because the surrounding
|
|
35
|
+
* render uses them structurally.
|
|
36
|
+
*/
|
|
37
|
+
export const neutralise = (value) => value
|
|
38
|
+
.replace(/[\r\n]+/g, " ")
|
|
39
|
+
.replace(/```+/g, "'''")
|
|
40
|
+
.replace(/[`]/g, "'")
|
|
41
|
+
.trim();
|
|
42
|
+
export const createSkillCatalogueProvider = (deps) => ({
|
|
43
|
+
id: SKILL_CATALOGUE_PROVIDER_ID,
|
|
44
|
+
async provide(context) {
|
|
45
|
+
const entries = await deps.resolver.listCatalog({
|
|
46
|
+
tenantId: context.tenantId,
|
|
47
|
+
assigned: deps.policy.assigned,
|
|
48
|
+
allowTenantSkills: deps.policy.allowTenantSkills,
|
|
49
|
+
});
|
|
50
|
+
// No section at all rather than an empty one. A heading saying "Skills" over nothing tells the model it has
|
|
51
|
+
// a capability and then shows it none, which is worse than silence — and it would still cost tokens.
|
|
52
|
+
if (entries.length === 0)
|
|
53
|
+
return [];
|
|
54
|
+
const lines = entries.map((e) => `- \`${neutralise(e.name)}\` — ${neutralise(e.description)}`);
|
|
55
|
+
const body = [
|
|
56
|
+
`You have ${entries.length} skill${entries.length === 1 ? "" : "s"} available. Each is a set of`,
|
|
57
|
+
"instructions for a particular kind of task. Read the list, and when one applies call",
|
|
58
|
+
"`load_skill` with its name to get the full instructions before you start.",
|
|
59
|
+
`You may load at most ${SKILL_LIMITS.maxLoadedPerRun} in a single run, so choose.`,
|
|
60
|
+
"",
|
|
61
|
+
...lines,
|
|
62
|
+
].join("\n");
|
|
63
|
+
return [
|
|
64
|
+
{
|
|
65
|
+
providerId: SKILL_CATALOGUE_PROVIDER_ID,
|
|
66
|
+
title: "Skills",
|
|
67
|
+
body,
|
|
68
|
+
// Above ordinary user context: a skill changes *how* the agent works, so it should survive pruning
|
|
69
|
+
// longer than the material it works on.
|
|
70
|
+
priority: 70,
|
|
71
|
+
estimatedTokens: estimateTokens(body),
|
|
72
|
+
provenance: "skill catalogue",
|
|
73
|
+
sensitivity: "internal",
|
|
74
|
+
// See the header: the section may instruct, and the customer-authored values inside it are neutralised
|
|
75
|
+
// rather than the whole section being disclaimed.
|
|
76
|
+
origin: "platform",
|
|
77
|
+
cacheable: true,
|
|
78
|
+
kind: "skills",
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
/**
|
|
84
|
+
* Adapts a `SkillResolver` to the registry's structural `SkillBodyLoader`.
|
|
85
|
+
*
|
|
86
|
+
* The version is not a parameter. `loadBody` pins to an exact version so a mid-run edit cannot change behaviour,
|
|
87
|
+
* and the version the model should get is the one the catalogue it just read advertised — so it is looked up
|
|
88
|
+
* here rather than trusted from the model's arguments. A model naming a version would be a model choosing which
|
|
89
|
+
* revision of an instruction to follow.
|
|
90
|
+
*/
|
|
91
|
+
export const createSkillBodyLoader = (deps) => ({
|
|
92
|
+
async load(context, name) {
|
|
93
|
+
const entries = await deps.resolver.listCatalog({
|
|
94
|
+
tenantId: context.tenantId,
|
|
95
|
+
assigned: deps.policy.assigned,
|
|
96
|
+
allowTenantSkills: deps.policy.allowTenantSkills,
|
|
97
|
+
});
|
|
98
|
+
// Resolved against the catalogue *this* agent may see, so `assigned` and `allowTenantSkills` gate loading
|
|
99
|
+
// and not merely listing. A policy that filtered the list but not the load would be no policy at all.
|
|
100
|
+
const entry = entries.find((e) => e.name === name);
|
|
101
|
+
if (entry === undefined)
|
|
102
|
+
return null;
|
|
103
|
+
const version = await deps.resolver.loadBody({ tenantId: context.tenantId, name, version: entry.version });
|
|
104
|
+
return { name: version.name, version: version.version, instructions: version.instructions };
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
//# sourceMappingURL=context.js.map
|
package/dist/skills/index.d.ts
CHANGED
package/dist/skills/index.js
CHANGED
package/dist/telemetry/spans.js
CHANGED
|
@@ -36,6 +36,11 @@ export const SPAN_FOR_RUN_EVENT = {
|
|
|
36
36
|
"question.answered": "hitl.question",
|
|
37
37
|
"approval.requested": "hitl.approval",
|
|
38
38
|
"approval.decided": "hitl.approval",
|
|
39
|
+
// Its own span rather than `hitl.approval`: both pause a run for a person, and the questions they answer are
|
|
40
|
+
// different — "may I do this" versus "I have no access, grant me some" — so a trace that merged them would
|
|
41
|
+
// make "how often do runs stall on a missing connection" unanswerable.
|
|
42
|
+
"connection.requested": "hitl.connection",
|
|
43
|
+
"connection.completed": "hitl.connection",
|
|
39
44
|
"usage.updated": "run.step",
|
|
40
45
|
"context.compacted": "context.compact",
|
|
41
46
|
// Its own span, not `run.step`: a guardrail verdict is the boundary of a decision somebody will need to find
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ArtifactExportStore` conformance (#134).
|
|
3
|
+
*
|
|
4
|
+
* Almost entirely about `claim`, because that is the method whose contract two adapters can plausibly get
|
|
5
|
+
* differently and where differing is expensive: a claim that is not exclusive renders the same PDF twice and
|
|
6
|
+
* leaves two rows pointing at two identical files, and a caller has no way to tell which is canonical.
|
|
7
|
+
*/
|
|
8
|
+
import type { ArtifactId, ConversationId, TenantId } from "../../core/ids.js";
|
|
9
|
+
import type { ArtifactExportStore } from "../../persistence/index.js";
|
|
10
|
+
import { type Fixture, type FixtureOrStore } from "./parents.js";
|
|
11
|
+
/** The parent an adapter with a foreign key needs: an artifact, which itself needs a conversation. */
|
|
12
|
+
export type ExportFixture = Fixture<ArtifactExportStore> & {
|
|
13
|
+
readonly seedArtifact?: (input: {
|
|
14
|
+
readonly tenantId: TenantId;
|
|
15
|
+
readonly artifactId: ArtifactId;
|
|
16
|
+
readonly conversationId: ConversationId;
|
|
17
|
+
}) => Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
export declare function artifactExportStoreConformance(make: () => FixtureOrStore<ArtifactExportStore> | Promise<FixtureOrStore<ArtifactExportStore>>): void;
|
|
20
|
+
//# sourceMappingURL=artifact-exports.d.ts.map
|