@m6d/cortex-cli 0.12.1 → 0.13.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 +135 -130
- package/package.json +16 -5
- package/src/cli.ts +17 -12
- package/src/commands/apply.ts +169 -0
- package/src/commands/{cc.ts → management.ts} +150 -100
- package/src/commands/new.ts +50 -74
- package/src/contracts/charts/chart-spec.ts +1 -1
- package/src/contracts/client-tools/index.ts +3 -3
- package/src/contracts/{runtime → datasets}/datasets.ts +7 -42
- package/src/contracts/datasets/index.ts +3 -0
- package/src/contracts/egress-policy/index.ts +16 -0
- package/src/contracts/{egress → egress-policy}/scrub.ts +34 -5
- package/src/contracts/{egress → egress-policy}/url.ts +22 -9
- package/src/contracts/errors/index.ts +38 -0
- package/src/contracts/json/index.ts +5 -0
- package/src/contracts/manifest/index.ts +932 -0
- package/src/contracts/manifest-file/index.ts +16 -0
- package/src/contracts/markdown/index.ts +1 -1
- package/src/contracts/pattern/index.ts +276 -0
- package/src/contracts/server/index.ts +247 -0
- package/src/contracts/server/reserved-names.fixtures.json +18 -0
- package/src/contracts/triggers/index.ts +112 -0
- package/src/contracts/wire/index.ts +61 -32
- package/src/{cc → management}/client.ts +19 -7
- package/src/{cc → management}/flags.ts +2 -1
- package/src/{cc → management}/profiles.ts +5 -5
- package/src/scaffold/files.ts +362 -237
- package/src/scaffold/ports.ts +13 -24
- package/src/scaffold/templates/datasets.ts.txt +52 -0
- package/src/scaffold/templates/dockerignore.txt +7 -0
- package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
- package/src/scaffold/templates/gitignore.txt +2 -1
- package/src/scaffold/templates/server.ts.txt +40 -0
- package/src/scaffold/templates/setup.ts.txt +52 -0
- package/src/ui/prompts.ts +4 -31
- package/src/ui/report.ts +5 -15
- package/tsconfig.json +2 -3
- package/src/commands/graph.ts +0 -228
- package/src/commands/swagger.ts +0 -247
- package/src/config/load.ts +0 -122
- package/src/config/validate.ts +0 -46
- package/src/contracts/credentials/index.ts +0 -183
- package/src/contracts/egress/index.ts +0 -120
- package/src/contracts/graph/clients/embed.ts +0 -62
- package/src/contracts/graph/clients/neo4j.ts +0 -85
- package/src/contracts/graph/helpers.ts +0 -55
- package/src/contracts/graph/index.ts +0 -30
- package/src/contracts/graph/schema.ts +0 -65
- package/src/contracts/graph/types.ts +0 -125
- package/src/contracts/rich-text/index.ts +0 -220
- package/src/contracts/runtime/api.ts +0 -345
- package/src/contracts/runtime/auth.ts +0 -41
- package/src/contracts/runtime/grammar.ts +0 -169
- package/src/contracts/runtime/index.ts +0 -15
- package/src/contracts/runtime/models.ts +0 -50
- package/src/contracts/runtime/signature.ts +0 -255
- package/src/contracts/runtime/sync.ts +0 -196
- package/src/contracts/runtime/triggers.ts +0 -267
- package/src/graph/expand-domains.ts +0 -286
- package/src/graph/generate-cypher.ts +0 -201
- package/src/graph/seed.ts +0 -199
- package/src/graph/validate.ts +0 -78
- package/src/scaffold/features.ts +0 -190
- package/src/scaffold/templates/domains-README.md.txt +0 -16
- package/src/scaffold/templates/index.ts.txt +0 -18
- package/src/swagger/extract-endpoints.ts +0 -269
- package/src/swagger/generated-block.ts +0 -74
- package/src/swagger/openapi-schema.ts +0 -168
- package/src/swagger/parse-response.ts +0 -88
- package/src/swagger/types.ts +0 -25
- /package/src/contracts/{runtime → datasets}/dataset-sql-guide.ts +0 -0
- /package/src/contracts/{runtime → datasets}/dataset-sql.ts +0 -0
- /package/src/contracts/{runtime → datasets}/datasets.fixtures.json +0 -0
- /package/src/{cc → management}/output.ts +0 -0
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
LOCALES,
|
|
4
|
-
MEDIA_FORMS,
|
|
5
|
-
TOOL_SURFACES,
|
|
6
|
-
type Greeting,
|
|
7
|
-
type Suggestions,
|
|
8
|
-
} from "../wire/index";
|
|
9
|
-
|
|
10
|
-
/*
|
|
11
|
-
* The grammar both sides of the runtime API share: identifier patterns, the
|
|
12
|
-
* closed sets, and the small schemas every request and response is built from.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/** The agent slug path param, validated on both sides of the wire. */
|
|
16
|
-
export const AGENT_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
17
|
-
export const agentSlugSchema = z.string().regex(AGENT_SLUG_PATTERN);
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* An ASCII JavaScript identifier: what a tool name (a `tools.<name>()` sandbox
|
|
21
|
-
* binding on the server) and a variable name must be, and what a rendered
|
|
22
|
-
* signature prints bare rather than quoted.
|
|
23
|
-
*/
|
|
24
|
-
export const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
25
|
-
|
|
26
|
-
/** Consumer-facing slugs: service keys, skill names, and trigger names share it. */
|
|
27
|
-
export const SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
28
|
-
export const slugSchema = z.string().regex(SLUG_PATTERN).max(64);
|
|
29
|
-
|
|
30
|
-
export const localeSchema = z.enum(LOCALES);
|
|
31
|
-
export const toolSurfaceSchema = z.enum(TOOL_SURFACES);
|
|
32
|
-
export const mediaFormSchema = z.enum(MEDIA_FORMS);
|
|
33
|
-
export const catalogVersionSchema = z.string().regex(/^[0-9a-f]{16}$/);
|
|
34
|
-
export const sha256HexSchema = z.string().regex(/^[0-9a-f]{64}$/);
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* A variable the server fills per request: a session value, a request-context
|
|
38
|
-
* value, or one the SDK computes. Declared once per server so the console
|
|
39
|
-
* offers exactly what that server provides to the agents created on it, in
|
|
40
|
-
* prompts and as tool input sources alike.
|
|
41
|
-
*/
|
|
42
|
-
export const variableDeclarationSchema = z.object({
|
|
43
|
-
name: z.string().regex(IDENTIFIER_PATTERN).max(64),
|
|
44
|
-
description: z.string().max(500),
|
|
45
|
-
example: z.string().max(200),
|
|
46
|
-
/** Computed by the SDK per request (`utcTime`, `locale`); no session value can set it. */
|
|
47
|
-
computed: z.boolean(),
|
|
48
|
-
});
|
|
49
|
-
export type VariableDeclaration = z.infer<typeof variableDeclarationSchema>;
|
|
50
|
-
|
|
51
|
-
/** Variable names a tool input or prompt reads. */
|
|
52
|
-
export const boundVariablesSchema = z.array(variableDeclarationSchema.shape.name);
|
|
53
|
-
|
|
54
|
-
/** A sandbox binding a piece of rich text mentions, so the turn can register it before the model reads the text. */
|
|
55
|
-
export const mentionedToolSchema = z.object({
|
|
56
|
-
toolId: z.uuid(),
|
|
57
|
-
name: z.string().regex(IDENTIFIER_PATTERN),
|
|
58
|
-
readOnly: z.boolean(),
|
|
59
|
-
/** A tool that parks for approval is never a sandbox binding, mentioned or not. */
|
|
60
|
-
requiresApproval: z.boolean(),
|
|
61
|
-
variables: boundVariablesSchema,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Variables the SDK itself fills for every agent, listed first in a server's
|
|
66
|
-
* declaration so the console offers them like any consumer-declared one.
|
|
67
|
-
*/
|
|
68
|
-
export const SDK_VARIABLES = [
|
|
69
|
-
{
|
|
70
|
-
name: "utcTime",
|
|
71
|
-
description: "Current time in UTC (ISO 8601)",
|
|
72
|
-
example: "2026-09-12T08:30:00Z",
|
|
73
|
-
computed: true,
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
name: "locale",
|
|
77
|
-
description: "Resolved locale of the session",
|
|
78
|
-
example: "ar",
|
|
79
|
-
computed: true,
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
name: "timezone",
|
|
83
|
-
description: "The user's IANA time zone, as the widget reports it",
|
|
84
|
-
example: "Asia/Riyadh",
|
|
85
|
-
computed: true,
|
|
86
|
-
},
|
|
87
|
-
] as const satisfies ReadonlyArray<VariableDeclaration>;
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* The widget's landing-page prompt chips, per locale: a short title the chip
|
|
91
|
-
* shows and the prompt it sends when clicked. Every source of them
|
|
92
|
-
* (the Control Center agent row, a server-code override, the capabilities
|
|
93
|
-
* handshake) stores this exact shape, so the widget never sees a blank or
|
|
94
|
-
* unbounded chip whichever source won. Pure validation, no transforms: a
|
|
95
|
-
* value that passes is served verbatim, so each source trims before it
|
|
96
|
-
* stores.
|
|
97
|
-
*/
|
|
98
|
-
export const SUGGESTION_LIMITS = { perLocale: 6, titleLength: 120, promptLength: 2000 } as const;
|
|
99
|
-
const trimmedText = (max: number) =>
|
|
100
|
-
z
|
|
101
|
-
.string()
|
|
102
|
-
.min(1)
|
|
103
|
-
.max(max)
|
|
104
|
-
.refine((text) => text === text.trim(), "text has surrounding whitespace");
|
|
105
|
-
export const suggestionsSchema = z.record(
|
|
106
|
-
localeSchema,
|
|
107
|
-
z
|
|
108
|
-
.array(
|
|
109
|
-
z.object({
|
|
110
|
-
title: trimmedText(SUGGESTION_LIMITS.titleLength),
|
|
111
|
-
prompt: trimmedText(SUGGESTION_LIMITS.promptLength),
|
|
112
|
-
}),
|
|
113
|
-
)
|
|
114
|
-
.max(SUGGESTION_LIMITS.perLocale),
|
|
115
|
-
) satisfies z.ZodType<Suggestions>;
|
|
116
|
-
|
|
117
|
-
/** One string per locale, at most 500 characters each; author copy the widget shows as is. */
|
|
118
|
-
export const localizedTextSchema = z.record(localeSchema, z.string().max(500));
|
|
119
|
-
|
|
120
|
-
export const GREETING_MAX_LENGTH = 200;
|
|
121
|
-
/**
|
|
122
|
-
* The blank-chat landing's headline per locale. Empty means the widget's
|
|
123
|
-
* built-in default for that locale; like suggestions, served verbatim.
|
|
124
|
-
*/
|
|
125
|
-
export const greetingSchema = z.record(
|
|
126
|
-
localeSchema,
|
|
127
|
-
z
|
|
128
|
-
.string()
|
|
129
|
-
.max(GREETING_MAX_LENGTH)
|
|
130
|
-
.refine((text) => text === text.trim(), "greeting has surrounding whitespace"),
|
|
131
|
-
) satisfies z.ZodType<Greeting>;
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* The Control Center's id for a resolved agent. Servers echo it on reports
|
|
135
|
-
* about that agent so they attach to it even if the slug or alias they used
|
|
136
|
-
* later moves.
|
|
137
|
-
*/
|
|
138
|
-
export const agentRefSchema = z.uuid();
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Response header carrying the resolved agent's ref on every agent-scoped
|
|
142
|
-
* runtime answer, errors included, so a server can attribute a request even
|
|
143
|
-
* when the body is an error envelope. Absent only when no agent resolved.
|
|
144
|
-
*/
|
|
145
|
-
export const AGENT_REF_HEADER = "X-CC-Agent-Ref";
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* The playground operator's stand-in for a test thread's session: a
|
|
149
|
-
* URI-encoded JSON object of declared variable values, sent by cc's
|
|
150
|
-
* playground beside `X-Cortex-Playground` and honoured only when that header
|
|
151
|
-
* verifies. The server stores it as the thread's session on creation, so the
|
|
152
|
-
* customer's `loadSessionData` (which never sees cc-minted tokens) is not
|
|
153
|
-
* needed for prompts and tool sources to have values.
|
|
154
|
-
*/
|
|
155
|
-
export const PLAYGROUND_SESSION_HEADER = "X-Cortex-Playground-Session";
|
|
156
|
-
|
|
157
|
-
/** Sent by cc's playground; a server verifies it against cc's JWKS before honouring it. */
|
|
158
|
-
export const PLAYGROUND_HEADER = "X-Cortex-Playground";
|
|
159
|
-
|
|
160
|
-
/** The end user's bearer, forwarded verbatim by the server to cc and by cc to the tool's backend. */
|
|
161
|
-
export const END_USER_TOKEN_HEADER = "X-End-User-Token";
|
|
162
|
-
|
|
163
|
-
/** Execute idempotency: a server-minted key the runtime replays within its window. */
|
|
164
|
-
export const IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
|
|
165
|
-
export const IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9._:-]{1,200}$/;
|
|
166
|
-
|
|
167
|
-
/** Correlation id echoed on every runtime response. */
|
|
168
|
-
export const REQUEST_ID_HEADER = "X-Request-Id";
|
|
169
|
-
export const REQUEST_ID_MAX_LENGTH = 128;
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* The contract between the cortex-cc console and `@m6d/cortex-server`:
|
|
3
|
-
* the grammar they share, the runtime API cc serves, triggers in both
|
|
4
|
-
* directions, and the fleet sync the server pushes.
|
|
5
|
-
*/
|
|
6
|
-
export * from "./grammar";
|
|
7
|
-
export * from "./auth";
|
|
8
|
-
export * from "./api";
|
|
9
|
-
export * from "./triggers";
|
|
10
|
-
export * from "./models";
|
|
11
|
-
export * from "./datasets";
|
|
12
|
-
export * from "./dataset-sql";
|
|
13
|
-
export * from "./dataset-sql-guide";
|
|
14
|
-
export * from "./sync";
|
|
15
|
-
export * from "./signature";
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* One level's pick of what a run talks to: a model a Control Center
|
|
5
|
-
* provider lists, the reasoning effort sent with it, both, or neither. An
|
|
6
|
-
* unset part inherits from the level above (trigger, then agent, then
|
|
7
|
-
* server, then the server's code config). An effort carries down only as
|
|
8
|
-
* far as the level that picked the model, and is sent only when that
|
|
9
|
-
* model's provider lists it.
|
|
10
|
-
*/
|
|
11
|
-
export const modelChoiceSchema = z
|
|
12
|
-
.object({
|
|
13
|
-
provider: z.string().min(1).nullable(),
|
|
14
|
-
model: z.string().min(1).nullable(),
|
|
15
|
-
effort: z.string().min(1).nullable(),
|
|
16
|
-
})
|
|
17
|
-
.refine((choice) => (choice.provider === null) === (choice.model === null), {
|
|
18
|
-
error: "A provider and its model are picked together.",
|
|
19
|
-
});
|
|
20
|
-
export type ModelChoice = z.infer<typeof modelChoiceSchema>;
|
|
21
|
-
|
|
22
|
-
/** A pick that inherits everything. */
|
|
23
|
-
export const INHERIT_MODEL = {
|
|
24
|
-
provider: null,
|
|
25
|
-
model: null,
|
|
26
|
-
effort: null,
|
|
27
|
-
} satisfies ModelChoice;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* An OpenAI-compatible endpoint as a server calls it: cc resolves the key
|
|
31
|
-
* from the provider's secret, and `efforts` are the values it accepts as
|
|
32
|
-
* `reasoning_effort`.
|
|
33
|
-
*/
|
|
34
|
-
export const modelProviderEndpointSchema = z.object({
|
|
35
|
-
baseURL: z.url(),
|
|
36
|
-
apiKey: z.string().min(1),
|
|
37
|
-
efforts: z.array(z.string().min(1)),
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* The model picks a server needs to run one agent: its server's and the
|
|
42
|
-
* agent's own (each trigger carries its own on its declaration), plus every
|
|
43
|
-
* provider any of them names.
|
|
44
|
-
*/
|
|
45
|
-
export const runtimeModelsSchema = z.object({
|
|
46
|
-
providers: z.record(z.string(), modelProviderEndpointSchema),
|
|
47
|
-
server: modelChoiceSchema,
|
|
48
|
-
agent: modelChoiceSchema,
|
|
49
|
-
});
|
|
50
|
-
export type RuntimeModels = z.infer<typeof runtimeModelsSchema>;
|
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import { isRecord } from "../json";
|
|
2
|
-
import { IDENTIFIER_PATTERN } from "./grammar";
|
|
3
|
-
|
|
4
|
-
/*
|
|
5
|
-
* The one rendering of a dynamic tool the model sees, whichever side
|
|
6
|
-
* produced it: Control Center renders its registry tools at resolve time,
|
|
7
|
-
* `@m6d/cortex-server` renders an MCP server's tools at turn time, and both
|
|
8
|
-
* land in the same "Dynamic Tools" prompt section. A TypeScript-like
|
|
9
|
-
* signature over the tool's JSON Schema: the input typed field by field with
|
|
10
|
-
* its description as a trailing comment, the response as the names of the
|
|
11
|
-
* fields it carries (what a script reads off the result), never their types.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/** Past this nesting an object prints as `object`: deeper detail costs more prompt than it earns. */
|
|
15
|
-
const MAX_DEPTH = 3;
|
|
16
|
-
|
|
17
|
-
function propertyName(name: string) {
|
|
18
|
-
return IDENTIFIER_PATTERN.test(name) ? name : JSON.stringify(name);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function compactLine(value: string) {
|
|
22
|
-
return value.replace(/\s+/g, " ").trim();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function fieldComment(value: unknown) {
|
|
26
|
-
if (typeof value !== "string") return "";
|
|
27
|
-
return Array.from(compactLine(value)).slice(0, 80).join("");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** The `required` names of an object schema, or none when it declares none. */
|
|
31
|
-
export function requiredProperties(schema: unknown) {
|
|
32
|
-
return isRecord(schema) && Array.isArray(schema.required)
|
|
33
|
-
? schema.required.filter((name): name is string => typeof name === "string")
|
|
34
|
-
: [];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Property names in the schema's `x-order` first, then the rest as declared. */
|
|
38
|
-
export function orderedPropertyNames(schema: Record<string, unknown>) {
|
|
39
|
-
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
40
|
-
const order = Array.isArray(schema["x-order"])
|
|
41
|
-
? schema["x-order"].filter(
|
|
42
|
-
(name): name is string => typeof name === "string" && name in properties,
|
|
43
|
-
)
|
|
44
|
-
: [];
|
|
45
|
-
return [...order, ...Object.keys(properties).filter((name) => !order.includes(name))];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const SCALAR_TYPES = new Set(["boolean", "number", "string"]);
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Follows a local `$ref` (`#`, `#/$defs/x`, or `#/definitions/x`) to its
|
|
52
|
-
* definition in the root document, keeping the keywords beside the ref (a
|
|
53
|
-
* description, say), so a shared shape a converter hoisted (a tool's schema
|
|
54
|
-
* rows) renders as its fields rather than `unknown`. A described ref comes out
|
|
55
|
-
* of zod as a one-entry `allOf`, which reads the same. A recursive shape ends
|
|
56
|
-
* where `MAX_DEPTH` ends it.
|
|
57
|
-
*/
|
|
58
|
-
function resolver(root: unknown) {
|
|
59
|
-
function definition(pointer: string) {
|
|
60
|
-
if (pointer === "#") return root;
|
|
61
|
-
const parts = pointer.split("/");
|
|
62
|
-
const [, section, name] = parts;
|
|
63
|
-
const definitions =
|
|
64
|
-
isRecord(root) && (section === "$defs" || section === "definitions")
|
|
65
|
-
? root[section]
|
|
66
|
-
: undefined;
|
|
67
|
-
return parts.length === 3 && isRecord(definitions) && name !== undefined
|
|
68
|
-
? definitions[name]
|
|
69
|
-
: undefined;
|
|
70
|
-
}
|
|
71
|
-
return function resolve(schema: unknown): unknown {
|
|
72
|
-
if (!isRecord(schema)) return schema;
|
|
73
|
-
const { $ref, allOf, ...rest } = schema;
|
|
74
|
-
if (typeof $ref === "string") {
|
|
75
|
-
const target = definition($ref);
|
|
76
|
-
return isRecord(target) ? { ...target, ...rest } : target;
|
|
77
|
-
}
|
|
78
|
-
if (Array.isArray(allOf) && allOf.length === 1) {
|
|
79
|
-
const target = resolve(allOf[0]);
|
|
80
|
-
return isRecord(target) ? { ...target, ...rest } : target;
|
|
81
|
-
}
|
|
82
|
-
return schema;
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
type Resolve = ReturnType<typeof resolver>;
|
|
87
|
-
|
|
88
|
-
function literalUnion(values: unknown[]) {
|
|
89
|
-
return values
|
|
90
|
-
.filter((value) => value === null || SCALAR_TYPES.has(typeof value))
|
|
91
|
-
.map((value) => JSON.stringify(value))
|
|
92
|
-
.join(" | ");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** An object's fields one per line, each typed, marked optional, and commented from its description. */
|
|
96
|
-
function inputObject(
|
|
97
|
-
schema: Record<string, unknown>,
|
|
98
|
-
resolve: Resolve,
|
|
99
|
-
depth: number,
|
|
100
|
-
indent: number,
|
|
101
|
-
) {
|
|
102
|
-
if (depth >= MAX_DEPTH) return "object";
|
|
103
|
-
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
104
|
-
const required = new Set(requiredProperties(schema));
|
|
105
|
-
const fields = orderedPropertyNames(schema).map((name) => {
|
|
106
|
-
const child = resolve(properties[name]);
|
|
107
|
-
const comment = isRecord(child) ? fieldComment(child.description) : "";
|
|
108
|
-
return `${" ".repeat(indent + 2)}${propertyName(name)}${required.has(name) ? "" : "?"}: ${inputType(child, resolve, depth + 1, indent + 2)},${comment ? ` // ${comment}` : ""}`;
|
|
109
|
-
});
|
|
110
|
-
return fields.length > 0 ? `{\n${fields.join("\n")}\n${" ".repeat(indent)}}` : "{}";
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function inputType(schema: unknown, resolve: Resolve, depth = 0, indent = 0): string {
|
|
114
|
-
schema = resolve(schema);
|
|
115
|
-
if (!isRecord(schema)) return "unknown";
|
|
116
|
-
if ("const" in schema && SCALAR_TYPES.has(typeof schema.const)) {
|
|
117
|
-
return JSON.stringify(schema.const);
|
|
118
|
-
}
|
|
119
|
-
if (Array.isArray(schema.enum) && schema.enum.length > 0) return literalUnion(schema.enum);
|
|
120
|
-
const variants = variantsOf(schema);
|
|
121
|
-
if (variants) {
|
|
122
|
-
const rendered = variants.map((variant) => inputType(variant, resolve, depth, indent));
|
|
123
|
-
return [...new Set(rendered)].join(" | ");
|
|
124
|
-
}
|
|
125
|
-
const type = primaryType(schema.type);
|
|
126
|
-
const nullable = allowsNull(schema.type) ? " | null" : "";
|
|
127
|
-
if (type === "null") return "null";
|
|
128
|
-
if (type === "array") {
|
|
129
|
-
const item = inputType(schema.items, resolve, depth + 1, indent);
|
|
130
|
-
// A union item type binds looser than `[]`, so it is parenthesized.
|
|
131
|
-
return `${item.includes(" | ") ? `(${item})` : item}[]${nullable}`;
|
|
132
|
-
}
|
|
133
|
-
if (type === "object" || isRecord(schema.properties)) {
|
|
134
|
-
return inputObject(schema, resolve, depth, indent) + nullable;
|
|
135
|
-
}
|
|
136
|
-
if (type === "integer") return `number${nullable}`;
|
|
137
|
-
return SCALAR_TYPES.has(String(type)) ? `${String(type)}${nullable}` : "unknown";
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** The non-null member of a JSON Schema `type`, so `["string", "null"]` reads as `string`. */
|
|
141
|
-
function primaryType(type: unknown) {
|
|
142
|
-
if (!Array.isArray(type)) return type;
|
|
143
|
-
const members: unknown[] = type;
|
|
144
|
-
return members.find((member) => member !== "null");
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function allowsNull(type: unknown) {
|
|
148
|
-
if (!Array.isArray(type)) return false;
|
|
149
|
-
const members: unknown[] = type;
|
|
150
|
-
return members.includes("null");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** The branches of a union schema, under whichever keyword carries them. */
|
|
154
|
-
function variantsOf(schema: Record<string, unknown>) {
|
|
155
|
-
const variants = [schema.oneOf, schema.anyOf].find(Array.isArray);
|
|
156
|
-
return variants && variants.length > 0 ? variants : undefined;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function isObjectSchema(schema: unknown): schema is Record<string, unknown> {
|
|
160
|
-
return (
|
|
161
|
-
isRecord(schema) && (primaryType(schema.type) === "object" || isRecord(schema.properties))
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* A response schema as one shape: a union's object branches merged into the
|
|
167
|
-
* fields any of them may carry, a `null` branch dropped. A script reads a
|
|
168
|
-
* field off whichever branch came back, so the names are what matter.
|
|
169
|
-
*/
|
|
170
|
-
function responseUnion(schema: unknown, resolve: Resolve): unknown {
|
|
171
|
-
const resolved = resolve(schema);
|
|
172
|
-
if (!isRecord(resolved)) return resolved;
|
|
173
|
-
const variants = variantsOf(resolved);
|
|
174
|
-
if (!variants) return resolved;
|
|
175
|
-
const branches = variants
|
|
176
|
-
.map((variant) => responseUnion(variant, resolve))
|
|
177
|
-
.filter(
|
|
178
|
-
(variant): variant is Record<string, unknown> =>
|
|
179
|
-
isRecord(variant) && variant.type !== "null",
|
|
180
|
-
);
|
|
181
|
-
const objects = branches.filter(isObjectSchema);
|
|
182
|
-
if (objects.length === 0) return branches[0];
|
|
183
|
-
const properties: Record<string, unknown> = {};
|
|
184
|
-
for (const object of objects) {
|
|
185
|
-
if (isRecord(object.properties)) Object.assign(properties, object.properties);
|
|
186
|
-
}
|
|
187
|
-
return { type: "object", properties };
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function responseObject(schema: Record<string, unknown>, resolve: Resolve, depth = 0): string {
|
|
191
|
-
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
192
|
-
const fields = orderedPropertyNames(schema).map((name) => {
|
|
193
|
-
const child = responseUnion(properties[name], resolve);
|
|
194
|
-
if (!isRecord(child) || depth >= MAX_DEPTH) return propertyName(name);
|
|
195
|
-
if (primaryType(child.type) === "array") {
|
|
196
|
-
const items = responseUnion(child.items, resolve);
|
|
197
|
-
return isObjectSchema(items)
|
|
198
|
-
? `${propertyName(name)}[].${responseObject(items, resolve, depth + 1)}`
|
|
199
|
-
: propertyName(name);
|
|
200
|
-
}
|
|
201
|
-
return isObjectSchema(child)
|
|
202
|
-
? `${propertyName(name)}.${responseObject(child, resolve, depth + 1)}`
|
|
203
|
-
: propertyName(name);
|
|
204
|
-
});
|
|
205
|
-
return fields.length > 0 ? `{ ${fields.join(", ")} }` : "{}";
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* The names a result carries, as a script would read them: `{ id, items[].{ name } }`.
|
|
210
|
-
* A schema that describes no fields is `{}`, which is also what a tool that
|
|
211
|
-
* declares no response gets; a caller that knows nothing passes `undefined`
|
|
212
|
-
* and gets `unknown`, so the model does not mistake silence for emptiness.
|
|
213
|
-
*/
|
|
214
|
-
export function responseShape(schema: unknown) {
|
|
215
|
-
if (schema === undefined) return "unknown";
|
|
216
|
-
const resolve = resolver(schema);
|
|
217
|
-
const root = responseUnion(schema, resolve);
|
|
218
|
-
if (!isRecord(root)) return "{}";
|
|
219
|
-
if (primaryType(root.type) === "array") {
|
|
220
|
-
const items = responseUnion(root.items, resolve);
|
|
221
|
-
return isObjectSchema(items) ? `[]${responseObject(items, resolve)}` : "[]";
|
|
222
|
-
}
|
|
223
|
-
return responseObject(root, resolve);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/** One line, no comment terminator, and always a sentence, so it can sit in the doc comment. */
|
|
227
|
-
function summarize(description: string) {
|
|
228
|
-
const text = compactLine(description).replaceAll("*/", "* /");
|
|
229
|
-
if (!text) return "";
|
|
230
|
-
return /[.!?]$/.test(text) ? text : `${text}.`;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export type ToolSignatureInput = {
|
|
234
|
-
/** As the script calls it: a name dot access cannot reach renders as the index call `tools["a-b"]`. */
|
|
235
|
-
name: string;
|
|
236
|
-
description: string;
|
|
237
|
-
/** JSON Schema of the input object as the model should see it. */
|
|
238
|
-
inputSchema: unknown;
|
|
239
|
-
/** Already rendered (a shared-shape ref like `$R1`), or the JSON Schema to render, or undefined for "unknown". */
|
|
240
|
-
returns: string | { schema: unknown };
|
|
241
|
-
/** Appended to the summary; the caller knows what else the model should be told (idempotency, embedded flows). */
|
|
242
|
-
notes?: ReadonlyArray<string>;
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
/** `/** summary *\/\nname(input: {...}): { fields }`. */
|
|
246
|
-
export function renderToolSignature(tool: ToolSignatureInput) {
|
|
247
|
-
const summary = [summarize(tool.description), ...(tool.notes ?? [])].filter(Boolean).join(" ");
|
|
248
|
-
const callee = IDENTIFIER_PATTERN.test(tool.name)
|
|
249
|
-
? tool.name
|
|
250
|
-
: `tools[${JSON.stringify(tool.name)}]`;
|
|
251
|
-
const returns =
|
|
252
|
-
typeof tool.returns === "string" ? tool.returns : responseShape(tool.returns.schema);
|
|
253
|
-
const input = inputType(tool.inputSchema, resolver(tool.inputSchema));
|
|
254
|
-
return `${summary ? `/** ${summary} */\n` : ""}${callee}(input: ${input}): ${returns}`;
|
|
255
|
-
}
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { MESSAGE_ROLES, type TokenUsage } from "../wire/index";
|
|
3
|
-
import { agentRefSchema, agentSlugSchema, variableDeclarationSchema } from "./grammar";
|
|
4
|
-
import { approvalRequestedEventSchema, TRIGGER_OUTCOMES, TRIGGER_SOURCES } from "./triggers";
|
|
5
|
-
import { DATASET_ERROR_KINDS } from "./datasets";
|
|
6
|
-
|
|
7
|
-
/*
|
|
8
|
-
* Fleet sync: `@m6d/cortex-server` mirrors thread/message/usage activity into
|
|
9
|
-
* cc via `POST /api/runtime/sync` batches. Upserts are idempotent on
|
|
10
|
-
* (serverId, id); an empty batch is the heartbeat.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const tokenCountSchema = z.number().int().nonnegative();
|
|
14
|
-
|
|
15
|
-
export const tokenUsageSchema = z.object({
|
|
16
|
-
input: z.object({
|
|
17
|
-
noCache: tokenCountSchema,
|
|
18
|
-
cacheRead: tokenCountSchema,
|
|
19
|
-
cacheWrite: tokenCountSchema,
|
|
20
|
-
total: tokenCountSchema,
|
|
21
|
-
}),
|
|
22
|
-
output: z.object({
|
|
23
|
-
reasoning: tokenCountSchema,
|
|
24
|
-
text: tokenCountSchema,
|
|
25
|
-
total: tokenCountSchema,
|
|
26
|
-
}),
|
|
27
|
-
total: tokenCountSchema,
|
|
28
|
-
}) satisfies z.ZodType<TokenUsage>;
|
|
29
|
-
|
|
30
|
-
const componentNamesSchema = z.array(z.string().min(1).max(128)).max(1_000);
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* What a server offers every agent the Control Center creates for it, sent
|
|
34
|
-
* whole at boot in `server.declare`: the `{{variables}}` its session loader
|
|
35
|
-
* and request context fill, SDK ones included, and the names of the
|
|
36
|
-
* components its code declares at server level, which reach those agents
|
|
37
|
-
* too and which the Control Center therefore refuses on its own components.
|
|
38
|
-
* Agents in the server's own code never appear here; the Control Center is
|
|
39
|
-
* blind to them.
|
|
40
|
-
*/
|
|
41
|
-
export const serverDeclarationSchema = z.object({
|
|
42
|
-
variables: z.array(variableDeclarationSchema),
|
|
43
|
-
components: z.object({
|
|
44
|
-
tool: componentNamesSchema,
|
|
45
|
-
skill: componentNamesSchema,
|
|
46
|
-
trigger: componentNamesSchema,
|
|
47
|
-
dataset_host: componentNamesSchema,
|
|
48
|
-
}),
|
|
49
|
-
});
|
|
50
|
-
export type ServerDeclaration = z.infer<typeof serverDeclarationSchema>;
|
|
51
|
-
|
|
52
|
-
export const syncEventSchema = z.discriminatedUnion("type", [
|
|
53
|
-
z.object({
|
|
54
|
-
type: z.literal("thread.upsert"),
|
|
55
|
-
id: z.uuid(),
|
|
56
|
-
agentSlug: agentSlugSchema,
|
|
57
|
-
userId: z.string().max(255),
|
|
58
|
-
title: z.string().max(256).nullable(),
|
|
59
|
-
isTest: z.boolean(),
|
|
60
|
-
/** Set on headless threads a trigger owns; `userId` is then a sentinel. */
|
|
61
|
-
triggerName: z.string().max(64).nullable(),
|
|
62
|
-
triggerKey: z.string().max(256).nullable(),
|
|
63
|
-
createdAt: z.iso.datetime(),
|
|
64
|
-
updatedAt: z.iso.datetime(),
|
|
65
|
-
}),
|
|
66
|
-
z.object({
|
|
67
|
-
type: z.literal("thread.delete"),
|
|
68
|
-
id: z.uuid(),
|
|
69
|
-
}),
|
|
70
|
-
z.object({
|
|
71
|
-
type: z.literal("message.upsert"),
|
|
72
|
-
id: z.string().max(128),
|
|
73
|
-
threadId: z.uuid(),
|
|
74
|
-
role: z.enum(MESSAGE_ROLES),
|
|
75
|
-
text: z.string().nullable(),
|
|
76
|
-
ordinal: z.number().int(),
|
|
77
|
-
content: z.looseObject({}),
|
|
78
|
-
createdAt: z.iso.datetime(),
|
|
79
|
-
}),
|
|
80
|
-
z.object({
|
|
81
|
-
type: z.literal("usage.record"),
|
|
82
|
-
id: z.uuid(),
|
|
83
|
-
messageId: z.string().max(128),
|
|
84
|
-
threadId: z.uuid(),
|
|
85
|
-
stepNumber: z.number().int(),
|
|
86
|
-
tokenUsage: tokenUsageSchema,
|
|
87
|
-
/** Stamped by the event constructor: llm_requests has no timestamp column, but time-bucketed usage stats need one. */
|
|
88
|
-
createdAt: z.iso.datetime(),
|
|
89
|
-
}),
|
|
90
|
-
/** One loadSkill call, hit or miss, from either skill source. Bounded by construction (no body travels). */
|
|
91
|
-
z.object({
|
|
92
|
-
type: z.literal("skill.load"),
|
|
93
|
-
id: z.uuid(),
|
|
94
|
-
threadId: z.uuid(),
|
|
95
|
-
agentSlug: agentSlugSchema,
|
|
96
|
-
/** The ref cc issued for the agent that answered; a code-only agent or a miss has none. */
|
|
97
|
-
agentRef: agentRefSchema.optional(),
|
|
98
|
-
/** As the model asked for it, even when nothing answered. */
|
|
99
|
-
skillName: z.string().max(64),
|
|
100
|
-
source: z.enum(TRIGGER_SOURCES).nullable(),
|
|
101
|
-
hit: z.boolean(),
|
|
102
|
-
/** The user message this turn answers; the mirror joins it for the drill-down. */
|
|
103
|
-
messageId: z.string().max(128),
|
|
104
|
-
createdAt: z.iso.datetime(),
|
|
105
|
-
}),
|
|
106
|
-
/** One dataset read, refused or answered; never the rows. */
|
|
107
|
-
z.object({
|
|
108
|
-
type: z.literal("dataset.read"),
|
|
109
|
-
id: z.uuid(),
|
|
110
|
-
threadId: z.uuid(),
|
|
111
|
-
agentSlug: agentSlugSchema,
|
|
112
|
-
agentRef: agentRefSchema.optional(),
|
|
113
|
-
host: z.string().max(64),
|
|
114
|
-
/** Empty when the statement was refused before it named one. */
|
|
115
|
-
dataset: z.string().max(64),
|
|
116
|
-
rowCount: z.number().int().nonnegative(),
|
|
117
|
-
truncated: z.boolean(),
|
|
118
|
-
durationMs: z.number().int().nonnegative(),
|
|
119
|
-
status: z.enum(["ok", ...DATASET_ERROR_KINDS]),
|
|
120
|
-
/** The SQL the model wrote, when the read came from a statement. */
|
|
121
|
-
sql: z.string().max(4000).optional(),
|
|
122
|
-
/** Why a read did not answer: the parser's refusal, the validator's issues or the host's detail. */
|
|
123
|
-
detail: z.string().max(1000).optional(),
|
|
124
|
-
/** The user or trigger message this turn answers; the mirror joins it for the drill-down. */
|
|
125
|
-
messageId: z.string().max(128),
|
|
126
|
-
createdAt: z.iso.datetime(),
|
|
127
|
-
}),
|
|
128
|
-
/** One firing, upserted on start and again on its terminal outcome; the firing id is the event id so both land on one mirror row. */
|
|
129
|
-
z.object({
|
|
130
|
-
type: z.literal("trigger.firing"),
|
|
131
|
-
id: z.uuid(),
|
|
132
|
-
agentSlug: agentSlugSchema,
|
|
133
|
-
agentRef: agentRefSchema.optional(),
|
|
134
|
-
triggerName: z.string().max(64),
|
|
135
|
-
source: z.enum(TRIGGER_SOURCES),
|
|
136
|
-
threadId: z.uuid().nullable(),
|
|
137
|
-
runId: z.string().max(64).nullable(),
|
|
138
|
-
outcome: z.enum(TRIGGER_OUTCOMES),
|
|
139
|
-
reason: z.string().max(64).nullable(),
|
|
140
|
-
output: z.unknown(),
|
|
141
|
-
createdAt: z.iso.datetime(),
|
|
142
|
-
finishedAt: z.iso.datetime().nullable(),
|
|
143
|
-
}),
|
|
144
|
-
/**
|
|
145
|
-
* A run on a cc-backed agent parked on approval. cc delivers it to the
|
|
146
|
-
* published webhooks in the agent's audience; a code agent reacts in
|
|
147
|
-
* code and sends none.
|
|
148
|
-
*/
|
|
149
|
-
z.object({
|
|
150
|
-
type: z.literal("approval.requested"),
|
|
151
|
-
id: z.uuid(),
|
|
152
|
-
agentSlug: agentSlugSchema,
|
|
153
|
-
agentRef: agentRefSchema,
|
|
154
|
-
payload: approvalRequestedEventSchema,
|
|
155
|
-
createdAt: z.iso.datetime(),
|
|
156
|
-
}),
|
|
157
|
-
/** What the server offers console-created agents, sent whole at boot and replacing the previous declaration. */
|
|
158
|
-
z.object({
|
|
159
|
-
type: z.literal("server.declare"),
|
|
160
|
-
id: z.uuid(),
|
|
161
|
-
...serverDeclarationSchema.shape,
|
|
162
|
-
createdAt: z.iso.datetime(),
|
|
163
|
-
}),
|
|
164
|
-
]);
|
|
165
|
-
export type SyncEvent = z.infer<typeof syncEventSchema>;
|
|
166
|
-
|
|
167
|
-
/*
|
|
168
|
-
* `epoch` identifies the sending database's generation (a UUIDv7 minted into
|
|
169
|
-
* the server DB's `sync_meta` row): a recreated database gets a new epoch, so
|
|
170
|
-
* cc can tell a legitimately restarted outbox sequence from a stale batch.
|
|
171
|
-
* v7 uuids are time-ordered, so cc also rejects batches from a superseded
|
|
172
|
-
* (lexicographically lower) epoch outright.
|
|
173
|
-
* `seq` is the batch's highest outbox row id; batches are contiguous
|
|
174
|
-
* id-ordered outbox prefixes, so within one epoch cc keeps a per-server
|
|
175
|
-
* high-water mark and discards batches at or below it. A stale in-flight
|
|
176
|
-
* batch (an expired flusher lease, a replay after a lost ack) can never
|
|
177
|
-
* overwrite newer mirrored state. Heartbeats carry no events and nothing to
|
|
178
|
-
* fence, so `seq` exists only on the non-empty branch.
|
|
179
|
-
*/
|
|
180
|
-
export const syncRequestSchema = z.union([
|
|
181
|
-
z.object({
|
|
182
|
-
events: z.array(syncEventSchema).min(1).max(500),
|
|
183
|
-
epoch: z.uuid(),
|
|
184
|
-
seq: z.number().int().positive(),
|
|
185
|
-
}),
|
|
186
|
-
z.object({
|
|
187
|
-
events: z.array(syncEventSchema).length(0),
|
|
188
|
-
epoch: z.uuid(),
|
|
189
|
-
}),
|
|
190
|
-
]);
|
|
191
|
-
export type SyncRequest = z.input<typeof syncRequestSchema>;
|
|
192
|
-
|
|
193
|
-
export const syncResponseSchema = z.object({
|
|
194
|
-
accepted: z.number().int(),
|
|
195
|
-
});
|
|
196
|
-
export type SyncResponse = z.infer<typeof syncResponseSchema>;
|