@sequenceholdings/agent-spec 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/dist/compiler.d.ts +50 -0
- package/dist/compiler.js +184 -0
- package/dist/index.d.ts +117 -0
- package/dist/index.js +248 -0
- package/dist/parse-agent-source.d.ts +10 -0
- package/dist/parse-agent-source.js +181 -0
- package/dist/yaml.d.ts +6 -0
- package/dist/yaml.js +29 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# `@sequenceholdings/agent-spec`
|
|
2
|
+
|
|
3
|
+
Preview SDK for authoring typed Sequence agent definitions.
|
|
4
|
+
|
|
5
|
+
The package is versioned independently and its API may change before a stable
|
|
6
|
+
release. Use Node.js 20 or later.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pnpm add @sequenceholdings/agent-spec
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Define an agent
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { defineAgent } from '@sequenceholdings/agent-spec'
|
|
18
|
+
|
|
19
|
+
export const agent = defineAgent({
|
|
20
|
+
id: '680000000000000000000001',
|
|
21
|
+
name: 'Support Agent',
|
|
22
|
+
model: { provider: 'anthropic', name: 'claude-sonnet-4-6' },
|
|
23
|
+
systemPrompt: 'Help the support team investigate and resolve requests.',
|
|
24
|
+
})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Definitions must be pure data literals: do not read environment variables, call
|
|
28
|
+
functions, or compute fields in `agent.ts`. Keep deployment targeting separate
|
|
29
|
+
from a definition, then use `seq-studio agents validate`, `plan`, and `apply`.
|
|
30
|
+
|
|
31
|
+
## Exports
|
|
32
|
+
|
|
33
|
+
- `@sequenceholdings/agent-spec` provides `defineAgent`,
|
|
34
|
+
`agentDefinitionSchema`, deterministic source formatting, and bundle hashing.
|
|
35
|
+
- `@sequenceholdings/agent-spec/compiler` compiles `agent.ts` files and deploy
|
|
36
|
+
manifests in Node.js. It statically extracts literal definitions and never
|
|
37
|
+
imports or executes agent source.
|
|
38
|
+
- `@sequenceholdings/agent-spec/yaml` provides `parseYamlAgent` only for
|
|
39
|
+
migrating legacy YAML definitions.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type AgentDefinition } from './index.js';
|
|
3
|
+
export declare const MAX_AGENT_FILES = 250;
|
|
4
|
+
export declare const MAX_AGENT_SOURCE_BYTES = 1000000;
|
|
5
|
+
export declare const agentDeployManifestSchema: z.ZodObject<{
|
|
6
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
7
|
+
definitions: z.ZodArray<z.ZodObject<{
|
|
8
|
+
id: z.ZodString;
|
|
9
|
+
path: z.ZodString;
|
|
10
|
+
environments: z.ZodArray<z.ZodString>;
|
|
11
|
+
}, z.core.$strip>>;
|
|
12
|
+
}, z.core.$strip>;
|
|
13
|
+
export type AgentDeployManifest = z.infer<typeof agentDeployManifestSchema>;
|
|
14
|
+
export type SelectedAgentEntry = {
|
|
15
|
+
id: string;
|
|
16
|
+
path: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function manifestEnvironments(manifest: AgentDeployManifest): Set<string>;
|
|
19
|
+
export declare function assertKnownTargetEnvironment({ manifest, environment, deployEnvironments, }: {
|
|
20
|
+
manifest: AgentDeployManifest;
|
|
21
|
+
environment: string;
|
|
22
|
+
deployEnvironments?: readonly string[];
|
|
23
|
+
}): void;
|
|
24
|
+
export declare function selectionStats({ manifest, environment, }: {
|
|
25
|
+
manifest: AgentDeployManifest;
|
|
26
|
+
environment?: string;
|
|
27
|
+
}): {
|
|
28
|
+
selected: number;
|
|
29
|
+
skipped: number;
|
|
30
|
+
total: number;
|
|
31
|
+
};
|
|
32
|
+
export declare function selectAgentEntries({ manifest, environment, }: {
|
|
33
|
+
manifest: AgentDeployManifest;
|
|
34
|
+
environment?: string;
|
|
35
|
+
}): SelectedAgentEntry[];
|
|
36
|
+
export interface CompiledAgentSource {
|
|
37
|
+
file: string;
|
|
38
|
+
definition: AgentDefinition;
|
|
39
|
+
}
|
|
40
|
+
export interface CompiledAgentBundle {
|
|
41
|
+
definitions: AgentDefinition[];
|
|
42
|
+
files: string[];
|
|
43
|
+
hash: string;
|
|
44
|
+
sources: CompiledAgentSource[];
|
|
45
|
+
}
|
|
46
|
+
export declare function compileAgentDirectory({ rootDir, filePaths, allowDuplicateIds, }: {
|
|
47
|
+
rootDir: string;
|
|
48
|
+
filePaths?: string[];
|
|
49
|
+
allowDuplicateIds?: boolean;
|
|
50
|
+
}): Promise<CompiledAgentBundle>;
|
package/dist/compiler.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { readdir, readFile, realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { agentDefinitionSchema, hashAgentBundle, } from './index.js';
|
|
5
|
+
import { parseAgentSource } from './parse-agent-source.js';
|
|
6
|
+
const objectIdPattern = /^[0-9a-f]{24}$/;
|
|
7
|
+
export const MAX_AGENT_FILES = 250;
|
|
8
|
+
export const MAX_AGENT_SOURCE_BYTES = 1_000_000;
|
|
9
|
+
function isWithinRoot({ root, candidate, }) {
|
|
10
|
+
const relativePath = relative(root, candidate);
|
|
11
|
+
return (relativePath !== '..' &&
|
|
12
|
+
!relativePath.startsWith(`..${sep}`) &&
|
|
13
|
+
!isAbsolute(relativePath));
|
|
14
|
+
}
|
|
15
|
+
const manifestPathSchema = z
|
|
16
|
+
.string()
|
|
17
|
+
.min(1)
|
|
18
|
+
.refine((path) => !path.startsWith('/') &&
|
|
19
|
+
!path.split('/').some((segment) => segment === '..') &&
|
|
20
|
+
path.endsWith('/agent.ts'), 'definition path must be a relative agent.ts path without parent traversal');
|
|
21
|
+
export const agentDeployManifestSchema = z.object({
|
|
22
|
+
schemaVersion: z.literal(1),
|
|
23
|
+
definitions: z.array(z.object({
|
|
24
|
+
id: z.string().regex(objectIdPattern),
|
|
25
|
+
path: manifestPathSchema,
|
|
26
|
+
environments: z.array(z.string().min(1)).min(1),
|
|
27
|
+
})),
|
|
28
|
+
});
|
|
29
|
+
export function manifestEnvironments(manifest) {
|
|
30
|
+
const names = new Set();
|
|
31
|
+
for (const entry of agentDeployManifestSchema.parse(manifest).definitions) {
|
|
32
|
+
for (const environment of entry.environments) {
|
|
33
|
+
if (environment !== '*')
|
|
34
|
+
names.add(environment);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return names;
|
|
38
|
+
}
|
|
39
|
+
export function assertKnownTargetEnvironment({ manifest, environment, deployEnvironments = [], }) {
|
|
40
|
+
const declared = manifestEnvironments(manifest);
|
|
41
|
+
if (declared.has(environment) || deployEnvironments.includes(environment)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const known = [...new Set([...declared, ...deployEnvironments])].sort();
|
|
45
|
+
const { selected, skipped, total } = selectionStats({ manifest, environment });
|
|
46
|
+
if (skipped === 0)
|
|
47
|
+
return;
|
|
48
|
+
throw new Error(`Unknown target environment "${environment}": it is not a deployment ` +
|
|
49
|
+
`environment and no manifest entry declares it, so ${skipped} of ${total} ` +
|
|
50
|
+
`agents would be skipped and only the ${selected} wildcard entries ` +
|
|
51
|
+
`deployed. Known environments: ${known.join(', ')}.`);
|
|
52
|
+
}
|
|
53
|
+
export function selectionStats({ manifest, environment, }) {
|
|
54
|
+
const total = new Set(agentDeployManifestSchema.parse(manifest).definitions.map((e) => e.id)).size;
|
|
55
|
+
const selected = selectAgentEntries({ manifest, environment }).length;
|
|
56
|
+
return { selected, skipped: total - selected, total };
|
|
57
|
+
}
|
|
58
|
+
export function selectAgentEntries({ manifest, environment, }) {
|
|
59
|
+
const parsed = agentDeployManifestSchema.parse(manifest);
|
|
60
|
+
if (environment === undefined) {
|
|
61
|
+
const byPath = new Map();
|
|
62
|
+
for (const entry of parsed.definitions) {
|
|
63
|
+
byPath.set(entry.path, { id: entry.id, path: entry.path });
|
|
64
|
+
}
|
|
65
|
+
return [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
66
|
+
}
|
|
67
|
+
const selected = new Map();
|
|
68
|
+
for (const entry of parsed.definitions) {
|
|
69
|
+
if (entry.environments.includes('*') ||
|
|
70
|
+
entry.environments.includes(environment)) {
|
|
71
|
+
selected.set(entry.id, entry.path);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return [...selected.entries()]
|
|
75
|
+
.map(([id, path]) => ({ id, path }))
|
|
76
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
77
|
+
}
|
|
78
|
+
async function discoverAgentFiles(rootDir) {
|
|
79
|
+
const files = [];
|
|
80
|
+
async function walk(directory) {
|
|
81
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
82
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
83
|
+
const path = join(directory, entry.name);
|
|
84
|
+
if (entry.isDirectory()) {
|
|
85
|
+
if (entry.name !== 'node_modules' && entry.name !== 'dist') {
|
|
86
|
+
await walk(path);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else if (entry.isFile() && entry.name === 'agent.ts') {
|
|
90
|
+
if (files.length >= MAX_AGENT_FILES) {
|
|
91
|
+
throw new Error(`Agent source directory contains too many agent.ts files (maximum ${MAX_AGENT_FILES})`);
|
|
92
|
+
}
|
|
93
|
+
files.push(path);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
await walk(rootDir);
|
|
98
|
+
return files;
|
|
99
|
+
}
|
|
100
|
+
async function resolveAgentFiles({ absoluteRoot, filePaths, }) {
|
|
101
|
+
if (filePaths.length > MAX_AGENT_FILES) {
|
|
102
|
+
throw new Error(`Agent source directory contains too many agent.ts files (maximum ${MAX_AGENT_FILES})`);
|
|
103
|
+
}
|
|
104
|
+
const files = await Promise.all(filePaths.map(async (path) => {
|
|
105
|
+
const requested = resolve(absoluteRoot, path);
|
|
106
|
+
if (!isWithinRoot({ root: absoluteRoot, candidate: requested })) {
|
|
107
|
+
throw new Error(`Agent source path escapes root: ${path}`);
|
|
108
|
+
}
|
|
109
|
+
const resolved = await realpath(requested);
|
|
110
|
+
if (!isWithinRoot({ root: absoluteRoot, candidate: resolved })) {
|
|
111
|
+
throw new Error(`Agent source path resolves outside root: ${path}`);
|
|
112
|
+
}
|
|
113
|
+
if (!(await stat(resolved)).isFile()) {
|
|
114
|
+
throw new Error(`Agent source path is not a file: ${path}`);
|
|
115
|
+
}
|
|
116
|
+
return resolved;
|
|
117
|
+
}));
|
|
118
|
+
return files.sort();
|
|
119
|
+
}
|
|
120
|
+
async function readAgentSource({ absoluteRoot, path, }) {
|
|
121
|
+
const resolved = await realpath(path);
|
|
122
|
+
if (!isWithinRoot({ root: absoluteRoot, candidate: resolved })) {
|
|
123
|
+
throw new Error(`Agent source path resolves outside root: ${path}`);
|
|
124
|
+
}
|
|
125
|
+
const metadata = await stat(resolved);
|
|
126
|
+
if (metadata.size > MAX_AGENT_SOURCE_BYTES) {
|
|
127
|
+
throw new Error(`Agent source ${relative(absoluteRoot, resolved)} exceeds the maximum size of ${MAX_AGENT_SOURCE_BYTES} bytes`);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
file: relative(absoluteRoot, resolved),
|
|
131
|
+
sourceText: await readFile(resolved, 'utf8'),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export async function compileAgentDirectory({ rootDir, filePaths, allowDuplicateIds = false, }) {
|
|
135
|
+
const absoluteRoot = await realpath(resolve(rootDir));
|
|
136
|
+
const files = filePaths === undefined
|
|
137
|
+
? await discoverAgentFiles(absoluteRoot)
|
|
138
|
+
: await resolveAgentFiles({ absoluteRoot, filePaths });
|
|
139
|
+
if (files.length === 0) {
|
|
140
|
+
return {
|
|
141
|
+
definitions: [],
|
|
142
|
+
files: [],
|
|
143
|
+
hash: hashAgentBundle({ definitions: [] }),
|
|
144
|
+
sources: [],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const sources = [];
|
|
148
|
+
for (const absoluteFile of files) {
|
|
149
|
+
const { file, sourceText } = await readAgentSource({
|
|
150
|
+
absoluteRoot,
|
|
151
|
+
path: absoluteFile,
|
|
152
|
+
});
|
|
153
|
+
const exports = parseAgentSource({ fileName: file, sourceText });
|
|
154
|
+
for (const row of exports) {
|
|
155
|
+
try {
|
|
156
|
+
sources.push({
|
|
157
|
+
file,
|
|
158
|
+
definition: agentDefinitionSchema.parse(row.value),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
throw new Error(`Invalid agent export ${row.exportName} in ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!allowDuplicateIds) {
|
|
167
|
+
const ids = new Set();
|
|
168
|
+
for (const { definition, file } of sources) {
|
|
169
|
+
if (ids.has(definition.id)) {
|
|
170
|
+
throw new Error(`Duplicate agent id "${definition.id}" in ${file}`);
|
|
171
|
+
}
|
|
172
|
+
ids.add(definition.id);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const definitions = sources
|
|
176
|
+
.map((source) => source.definition)
|
|
177
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
178
|
+
return {
|
|
179
|
+
definitions,
|
|
180
|
+
files: files.map((file) => relative(absoluteRoot, file)),
|
|
181
|
+
hash: hashAgentBundle({ definitions }),
|
|
182
|
+
sources,
|
|
183
|
+
};
|
|
184
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export type UnsupportedConstraint = {
|
|
3
|
+
path: string;
|
|
4
|
+
type: string;
|
|
5
|
+
keyword: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function collectUnsupportedConstraints({ schema, path, }: {
|
|
8
|
+
schema: unknown;
|
|
9
|
+
path?: string;
|
|
10
|
+
}): UnsupportedConstraint[];
|
|
11
|
+
export declare const AGENT_PERMISSION_ROLES: readonly ["owner", "editor", "invoker", "outputs_viewer"];
|
|
12
|
+
export type AgentPermissionRole = (typeof AGENT_PERMISSION_ROLES)[number];
|
|
13
|
+
export declare const agentDefinitionSchema: z.ZodObject<{
|
|
14
|
+
id: z.ZodString;
|
|
15
|
+
name: z.ZodString;
|
|
16
|
+
description: z.ZodOptional<z.ZodString>;
|
|
17
|
+
framework: z.ZodOptional<z.ZodEnum<{
|
|
18
|
+
vercel: "vercel";
|
|
19
|
+
"claude-agent": "claude-agent";
|
|
20
|
+
}>>;
|
|
21
|
+
vmBackend: z.ZodOptional<z.ZodEnum<{
|
|
22
|
+
gce: "gce";
|
|
23
|
+
e2b: "e2b";
|
|
24
|
+
}>>;
|
|
25
|
+
idleTimeoutSeconds: z.ZodOptional<z.ZodNumber>;
|
|
26
|
+
agentCapabilities: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
27
|
+
claudeToolSearch: z.ZodOptional<z.ZodEnum<{
|
|
28
|
+
enabled: "enabled";
|
|
29
|
+
disabled: "disabled";
|
|
30
|
+
auto: "auto";
|
|
31
|
+
}>>;
|
|
32
|
+
claudeBuiltInTools: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
33
|
+
openaiCodeInterpreter: z.ZodOptional<z.ZodNullable<z.ZodPreprocess<z.ZodObject<{
|
|
34
|
+
enabled: z.ZodBoolean;
|
|
35
|
+
containerId: z.ZodOptional<z.ZodString>;
|
|
36
|
+
fileIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
37
|
+
}, z.core.$strip>>>>;
|
|
38
|
+
model: z.ZodDefault<z.ZodObject<{
|
|
39
|
+
provider: z.ZodString;
|
|
40
|
+
name: z.ZodString;
|
|
41
|
+
}, z.core.$strip>>;
|
|
42
|
+
thinking: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
43
|
+
reasoningEffort: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
44
|
+
temperature: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
45
|
+
systemPrompt: z.ZodOptional<z.ZodString>;
|
|
46
|
+
tools: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
47
|
+
mcpServers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
48
|
+
requiredMcpServers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
49
|
+
excludedMcpTools: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
50
|
+
hideIntermediateText: z.ZodOptional<z.ZodBoolean>;
|
|
51
|
+
middleware: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
52
|
+
subagents: z.ZodDefault<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
53
|
+
id: z.ZodString;
|
|
54
|
+
forkContext: z.ZodOptional<z.ZodBoolean>;
|
|
55
|
+
}, z.core.$strip>]>>>;
|
|
56
|
+
skills: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
57
|
+
contextFileIds: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
58
|
+
structuredOutput: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
59
|
+
inputSchema: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
60
|
+
fields: z.ZodArray<z.ZodObject<{
|
|
61
|
+
name: z.ZodString;
|
|
62
|
+
label: z.ZodOptional<z.ZodString>;
|
|
63
|
+
description: z.ZodOptional<z.ZodString>;
|
|
64
|
+
type: z.ZodEnum<{
|
|
65
|
+
string: "string";
|
|
66
|
+
number: "number";
|
|
67
|
+
boolean: "boolean";
|
|
68
|
+
enum: "enum";
|
|
69
|
+
text: "text";
|
|
70
|
+
}>;
|
|
71
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
72
|
+
default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
73
|
+
enum: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
74
|
+
placeholder: z.ZodOptional<z.ZodString>;
|
|
75
|
+
}, z.core.$strip>>;
|
|
76
|
+
}, z.core.$strip>>>;
|
|
77
|
+
promptTemplate: z.ZodOptional<z.ZodString>;
|
|
78
|
+
webhooks: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
79
|
+
mcpServerConfigs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
80
|
+
aiCheckerConfig: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
81
|
+
stubConfig: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
82
|
+
maxTtlSeconds: z.ZodOptional<z.ZodNumber>;
|
|
83
|
+
agentPermissions: z.ZodOptional<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
84
|
+
apiPermissions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
85
|
+
managedBy: z.ZodOptional<z.ZodNullable<z.ZodLiteral<"automation">>>;
|
|
86
|
+
isPublic: z.ZodOptional<z.ZodBoolean>;
|
|
87
|
+
publicOutputs: z.ZodOptional<z.ZodBoolean>;
|
|
88
|
+
permissions: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
89
|
+
type: z.ZodEnum<{
|
|
90
|
+
user: "user";
|
|
91
|
+
group: "group";
|
|
92
|
+
}>;
|
|
93
|
+
id: z.ZodString;
|
|
94
|
+
role: z.ZodEnum<{
|
|
95
|
+
owner: "owner";
|
|
96
|
+
editor: "editor";
|
|
97
|
+
invoker: "invoker";
|
|
98
|
+
outputs_viewer: "outputs_viewer";
|
|
99
|
+
}>;
|
|
100
|
+
}, z.core.$strip>>>;
|
|
101
|
+
}, z.core.$strict>;
|
|
102
|
+
export type AgentDefinition = z.infer<typeof agentDefinitionSchema>;
|
|
103
|
+
export type AgentDefinitionInput = z.input<typeof agentDefinitionSchema>;
|
|
104
|
+
export declare function defineAgent(input: AgentDefinitionInput): AgentDefinition;
|
|
105
|
+
export declare function canonicalJson(value: unknown): string;
|
|
106
|
+
export declare function canonicalAgentJson(definition: AgentDefinition): string;
|
|
107
|
+
export declare function hashAgentBundle({ definitions, }: {
|
|
108
|
+
definitions: AgentDefinition[];
|
|
109
|
+
}): string;
|
|
110
|
+
export declare function formatAgentSource({ definition, exportName, }: {
|
|
111
|
+
definition: unknown;
|
|
112
|
+
exportName?: string;
|
|
113
|
+
}): string;
|
|
114
|
+
export declare function generateAgentSource({ definition, exportName, }: {
|
|
115
|
+
definition: AgentDefinition;
|
|
116
|
+
exportName?: string;
|
|
117
|
+
}): string;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
const OBJECT_ID_PATTERN = /^[0-9a-f]{24}$/;
|
|
4
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
5
|
+
const DEFAULT_MODEL = {
|
|
6
|
+
provider: 'anthropic',
|
|
7
|
+
name: 'claude-sonnet-4-5-20250929',
|
|
8
|
+
};
|
|
9
|
+
const jsonObjectSchema = z.record(z.string(), z.unknown());
|
|
10
|
+
const unsupportedConstraints = {
|
|
11
|
+
integer: ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum'],
|
|
12
|
+
number: ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum'],
|
|
13
|
+
array: ['minItems', 'maxItems', 'uniqueItems'],
|
|
14
|
+
string: ['minLength', 'maxLength', 'format', 'pattern'],
|
|
15
|
+
};
|
|
16
|
+
export function collectUnsupportedConstraints({ schema, path = '', }) {
|
|
17
|
+
if (schema === null || typeof schema !== 'object')
|
|
18
|
+
return [];
|
|
19
|
+
const object = schema;
|
|
20
|
+
const type = typeof object.type === 'string' ? object.type : undefined;
|
|
21
|
+
const violations = [];
|
|
22
|
+
if (type !== undefined) {
|
|
23
|
+
for (const keyword of unsupportedConstraints[type] ?? []) {
|
|
24
|
+
if (keyword in object) {
|
|
25
|
+
violations.push({ path: path || '(root)', type, keyword });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (type === 'object' &&
|
|
30
|
+
object.properties !== null &&
|
|
31
|
+
typeof object.properties === 'object') {
|
|
32
|
+
for (const [key, value] of Object.entries(object.properties)) {
|
|
33
|
+
violations.push(...collectUnsupportedConstraints({
|
|
34
|
+
schema: value,
|
|
35
|
+
path: `${path}.properties.${key}`,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (type === 'array' && object.items !== undefined) {
|
|
40
|
+
violations.push(...collectUnsupportedConstraints({
|
|
41
|
+
schema: object.items,
|
|
42
|
+
path: `${path}.items`,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
for (const combinator of ['anyOf', 'oneOf', 'allOf', 'prefixItems']) {
|
|
46
|
+
const entries = object[combinator];
|
|
47
|
+
if (!Array.isArray(entries))
|
|
48
|
+
continue;
|
|
49
|
+
entries.forEach((entry, index) => {
|
|
50
|
+
violations.push(...collectUnsupportedConstraints({
|
|
51
|
+
schema: entry,
|
|
52
|
+
path: `${path}.${combinator}[${index}]`,
|
|
53
|
+
}));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return violations;
|
|
57
|
+
}
|
|
58
|
+
const modelSchema = z.object({
|
|
59
|
+
provider: z.string().min(1),
|
|
60
|
+
name: z.string().min(1),
|
|
61
|
+
});
|
|
62
|
+
export const AGENT_PERMISSION_ROLES = [
|
|
63
|
+
'owner',
|
|
64
|
+
'editor',
|
|
65
|
+
'invoker',
|
|
66
|
+
'outputs_viewer',
|
|
67
|
+
];
|
|
68
|
+
const permissionGrantSchema = z.object({
|
|
69
|
+
type: z.enum(['user', 'group']),
|
|
70
|
+
id: z.string().min(1),
|
|
71
|
+
role: z.enum(AGENT_PERMISSION_ROLES),
|
|
72
|
+
});
|
|
73
|
+
const subagentSchema = z.union([
|
|
74
|
+
z.string().min(1),
|
|
75
|
+
z.object({
|
|
76
|
+
id: z.string().min(1),
|
|
77
|
+
forkContext: z.boolean().optional(),
|
|
78
|
+
}),
|
|
79
|
+
]);
|
|
80
|
+
const inputFieldSchema = z
|
|
81
|
+
.object({
|
|
82
|
+
name: z
|
|
83
|
+
.string()
|
|
84
|
+
.min(1)
|
|
85
|
+
.regex(/^[A-Za-z_][A-Za-z0-9_]*$/, 'name must be a valid identifier'),
|
|
86
|
+
label: z.string().optional(),
|
|
87
|
+
description: z.string().optional(),
|
|
88
|
+
type: z.enum(['string', 'text', 'number', 'boolean', 'enum']),
|
|
89
|
+
required: z.boolean().optional(),
|
|
90
|
+
default: z.union([z.string(), z.number(), z.boolean()]).optional(),
|
|
91
|
+
enum: z.array(z.string()).optional(),
|
|
92
|
+
placeholder: z.string().optional(),
|
|
93
|
+
})
|
|
94
|
+
.superRefine((field, context) => {
|
|
95
|
+
if (field.type === 'enum') {
|
|
96
|
+
if (field.enum === undefined || field.enum.length === 0) {
|
|
97
|
+
context.addIssue({
|
|
98
|
+
code: 'custom',
|
|
99
|
+
path: ['enum'],
|
|
100
|
+
message: 'enum fields require at least one option',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (field.default !== undefined &&
|
|
104
|
+
(typeof field.default !== 'string' ||
|
|
105
|
+
!field.enum?.includes(field.default))) {
|
|
106
|
+
context.addIssue({
|
|
107
|
+
code: 'custom',
|
|
108
|
+
path: ['default'],
|
|
109
|
+
message: 'default must be one of the declared enum options',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (field.default === undefined)
|
|
115
|
+
return;
|
|
116
|
+
const expected = field.type === 'number'
|
|
117
|
+
? 'number'
|
|
118
|
+
: field.type === 'boolean'
|
|
119
|
+
? 'boolean'
|
|
120
|
+
: 'string';
|
|
121
|
+
if (typeof field.default !== expected) {
|
|
122
|
+
context.addIssue({
|
|
123
|
+
code: 'custom',
|
|
124
|
+
path: ['default'],
|
|
125
|
+
message: `default for ${field.type} fields must be a ${expected}`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
const inputSchemaSchema = z
|
|
130
|
+
.object({ fields: z.array(inputFieldSchema) })
|
|
131
|
+
.refine((schema) => new Set(schema.fields.map((field) => field.name)).size ===
|
|
132
|
+
schema.fields.length, {
|
|
133
|
+
path: ['fields'],
|
|
134
|
+
message: 'fields[].name must be unique within an inputSchema',
|
|
135
|
+
});
|
|
136
|
+
const openaiCodeInterpreterSchema = z.preprocess((value) => {
|
|
137
|
+
if (value === true)
|
|
138
|
+
return { enabled: true };
|
|
139
|
+
if (value === false)
|
|
140
|
+
return { enabled: false };
|
|
141
|
+
return value;
|
|
142
|
+
}, z.object({
|
|
143
|
+
enabled: z.boolean(),
|
|
144
|
+
containerId: z.string().min(1).optional(),
|
|
145
|
+
fileIds: z.array(z.string().min(1)).optional(),
|
|
146
|
+
}));
|
|
147
|
+
export const agentDefinitionSchema = z
|
|
148
|
+
.strictObject({
|
|
149
|
+
id: z
|
|
150
|
+
.string()
|
|
151
|
+
.regex(OBJECT_ID_PATTERN, 'id must be a 24-character lowercase hex ObjectId'),
|
|
152
|
+
name: z.string().min(1),
|
|
153
|
+
description: z.string().optional(),
|
|
154
|
+
framework: z.enum(['vercel', 'claude-agent']).optional(),
|
|
155
|
+
vmBackend: z.enum(['gce', 'e2b']).optional(),
|
|
156
|
+
idleTimeoutSeconds: z.number().int().min(60).max(86_400).optional(),
|
|
157
|
+
agentCapabilities: jsonObjectSchema.nullish(),
|
|
158
|
+
claudeToolSearch: z.enum(['enabled', 'disabled', 'auto']).optional(),
|
|
159
|
+
claudeBuiltInTools: z.array(z.string()).default([]),
|
|
160
|
+
openaiCodeInterpreter: openaiCodeInterpreterSchema.nullish(),
|
|
161
|
+
model: modelSchema.default(DEFAULT_MODEL),
|
|
162
|
+
thinking: jsonObjectSchema.nullish(),
|
|
163
|
+
reasoningEffort: z.string().nullish(),
|
|
164
|
+
temperature: z.number().min(0).max(1).nullish(),
|
|
165
|
+
systemPrompt: z.string().optional(),
|
|
166
|
+
tools: z.array(z.string()).default([]),
|
|
167
|
+
mcpServers: z.array(z.string()).default([]),
|
|
168
|
+
requiredMcpServers: z.array(z.string()).default([]),
|
|
169
|
+
excludedMcpTools: z.array(z.string()).default([]),
|
|
170
|
+
hideIntermediateText: z.boolean().optional(),
|
|
171
|
+
middleware: z.array(z.string()).default([]),
|
|
172
|
+
subagents: z.array(subagentSchema).default([]),
|
|
173
|
+
skills: z.array(z.string()).default([]),
|
|
174
|
+
contextFileIds: z.array(z.string()).default([]),
|
|
175
|
+
structuredOutput: jsonObjectSchema.optional(),
|
|
176
|
+
inputSchema: inputSchemaSchema.nullish(),
|
|
177
|
+
promptTemplate: z.string().optional(),
|
|
178
|
+
webhooks: jsonObjectSchema.optional(),
|
|
179
|
+
mcpServerConfigs: jsonObjectSchema.optional(),
|
|
180
|
+
aiCheckerConfig: jsonObjectSchema.optional(),
|
|
181
|
+
stubConfig: jsonObjectSchema.nullish(),
|
|
182
|
+
maxTtlSeconds: z.number().int().positive().optional(),
|
|
183
|
+
agentPermissions: z.array(jsonObjectSchema).optional(),
|
|
184
|
+
apiPermissions: z.array(z.string()).optional(),
|
|
185
|
+
managedBy: z.literal('automation').nullish(),
|
|
186
|
+
isPublic: z.boolean().optional(),
|
|
187
|
+
publicOutputs: z.boolean().optional(),
|
|
188
|
+
permissions: z.array(permissionGrantSchema).optional(),
|
|
189
|
+
})
|
|
190
|
+
.superRefine((definition, context) => {
|
|
191
|
+
for (const [index, serverId] of definition.requiredMcpServers.entries()) {
|
|
192
|
+
if (definition.mcpServers.includes(serverId))
|
|
193
|
+
continue;
|
|
194
|
+
context.addIssue({
|
|
195
|
+
code: 'custom',
|
|
196
|
+
path: ['requiredMcpServers', index],
|
|
197
|
+
message: `requiredMcpServers entry "${serverId}" must also appear in mcpServers`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (definition.structuredOutput?.enabled === true) {
|
|
201
|
+
const schema = definition.structuredOutput.schema;
|
|
202
|
+
for (const violation of collectUnsupportedConstraints({ schema })) {
|
|
203
|
+
context.addIssue({
|
|
204
|
+
code: 'custom',
|
|
205
|
+
path: ['structuredOutput', 'schema'],
|
|
206
|
+
message: `${violation.path}: "${violation.keyword}" on ${violation.type} is not supported`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
export function defineAgent(input) {
|
|
212
|
+
return agentDefinitionSchema.parse(input);
|
|
213
|
+
}
|
|
214
|
+
function sortJson(value) {
|
|
215
|
+
if (Array.isArray(value))
|
|
216
|
+
return value.map(sortJson);
|
|
217
|
+
if (value === null || typeof value !== 'object')
|
|
218
|
+
return value;
|
|
219
|
+
const object = value;
|
|
220
|
+
return Object.fromEntries(Object.keys(object)
|
|
221
|
+
.sort()
|
|
222
|
+
.filter((key) => object[key] !== undefined)
|
|
223
|
+
.map((key) => [key, sortJson(object[key])]));
|
|
224
|
+
}
|
|
225
|
+
export function canonicalJson(value) {
|
|
226
|
+
return JSON.stringify(sortJson(value));
|
|
227
|
+
}
|
|
228
|
+
export function canonicalAgentJson(definition) {
|
|
229
|
+
return canonicalJson(definition);
|
|
230
|
+
}
|
|
231
|
+
export function hashAgentBundle({ definitions, }) {
|
|
232
|
+
const canonical = [...definitions]
|
|
233
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
234
|
+
.map(canonicalAgentJson)
|
|
235
|
+
.join('\n');
|
|
236
|
+
return createHash('sha256').update('sequence-agent-bundle-v1\0').update(canonical).digest('hex');
|
|
237
|
+
}
|
|
238
|
+
export function formatAgentSource({ definition, exportName = 'agent', }) {
|
|
239
|
+
if (!IDENTIFIER_PATTERN.test(exportName)) {
|
|
240
|
+
throw new Error('exportName must be a valid TypeScript identifier');
|
|
241
|
+
}
|
|
242
|
+
const object = JSON.stringify(sortJson(definition), null, 2);
|
|
243
|
+
return ("import { defineAgent } from '@sequenceholdings/agent-spec'\n\n" +
|
|
244
|
+
`export const ${exportName} = defineAgent(${object})\n`);
|
|
245
|
+
}
|
|
246
|
+
export function generateAgentSource({ definition, exportName = 'agent', }) {
|
|
247
|
+
return formatAgentSource({ definition: defineAgent(definition), exportName });
|
|
248
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
export type ParsedAgentExport = {
|
|
3
|
+
exportName: string;
|
|
4
|
+
literalNode: ts.ObjectLiteralExpression;
|
|
5
|
+
value: unknown;
|
|
6
|
+
};
|
|
7
|
+
export declare function parseAgentSource({ fileName, sourceText, }: {
|
|
8
|
+
fileName: string;
|
|
9
|
+
sourceText: string;
|
|
10
|
+
}): ParsedAgentExport[];
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
export function parseAgentSource({ fileName, sourceText, }) {
|
|
3
|
+
const sourceFile = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
4
|
+
const parseDiagnostics = sourceFile.parseDiagnostics ?? [];
|
|
5
|
+
const parseErrors = parseDiagnostics.filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
|
|
6
|
+
if (parseErrors.length > 0) {
|
|
7
|
+
const first = parseErrors[0];
|
|
8
|
+
const message = first
|
|
9
|
+
? ts.flattenDiagnosticMessageText(first.messageText, '\n')
|
|
10
|
+
: 'unknown parse error';
|
|
11
|
+
throw new Error(`${fileName}: invalid TypeScript — ${message}`);
|
|
12
|
+
}
|
|
13
|
+
const bindings = collectConstObjectBindings(sourceFile);
|
|
14
|
+
const found = [];
|
|
15
|
+
const seenLiterals = new Set();
|
|
16
|
+
const take = ({ exportName, expression, }) => {
|
|
17
|
+
const literal = resolveObjectLiteral(expression, bindings);
|
|
18
|
+
if (!literal) {
|
|
19
|
+
throw new Error(`${fileName}: export "${exportName}" must be a defineAgent({...}) ` +
|
|
20
|
+
`call or an object literal — executable expressions are not allowed`);
|
|
21
|
+
}
|
|
22
|
+
if (seenLiterals.has(literal))
|
|
23
|
+
return;
|
|
24
|
+
seenLiterals.add(literal);
|
|
25
|
+
found.push({
|
|
26
|
+
exportName,
|
|
27
|
+
literalNode: literal,
|
|
28
|
+
value: objectLiteralToValue(literal, fileName),
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
for (const statement of sourceFile.statements) {
|
|
32
|
+
if (ts.isVariableStatement(statement)) {
|
|
33
|
+
const isExport = statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
34
|
+
if (!isExport)
|
|
35
|
+
continue;
|
|
36
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
37
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
take({
|
|
41
|
+
exportName: declaration.name.text,
|
|
42
|
+
expression: declaration.initializer,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
|
|
48
|
+
take({ exportName: 'default', expression: statement.expression });
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (ts.isExportDeclaration(statement) && statement.exportClause) {
|
|
52
|
+
if (!ts.isNamedExports(statement.exportClause))
|
|
53
|
+
continue;
|
|
54
|
+
for (const element of statement.exportClause.elements) {
|
|
55
|
+
const localName = (element.propertyName ?? element.name).text;
|
|
56
|
+
const binding = bindings.get(localName);
|
|
57
|
+
if (!binding) {
|
|
58
|
+
throw new Error(`${fileName}: export "${element.name.text}" does not resolve to ` +
|
|
59
|
+
`a local object literal`);
|
|
60
|
+
}
|
|
61
|
+
take({ exportName: element.name.text, expression: binding });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (found.length === 0) {
|
|
66
|
+
throw new Error(`${fileName}: no agent definition export found (expected ` +
|
|
67
|
+
`export const agent = defineAgent({...}) or an object literal)`);
|
|
68
|
+
}
|
|
69
|
+
return found;
|
|
70
|
+
}
|
|
71
|
+
function collectConstObjectBindings(sourceFile) {
|
|
72
|
+
const bindings = new Map();
|
|
73
|
+
for (const statement of sourceFile.statements) {
|
|
74
|
+
if (!ts.isVariableStatement(statement))
|
|
75
|
+
continue;
|
|
76
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
77
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const literal = unwrapDefineAgent(declaration.initializer);
|
|
81
|
+
if (literal)
|
|
82
|
+
bindings.set(declaration.name.text, literal);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return bindings;
|
|
86
|
+
}
|
|
87
|
+
function resolveObjectLiteral(expression, bindings) {
|
|
88
|
+
const unwrapped = unwrapDefineAgent(expression);
|
|
89
|
+
if (unwrapped)
|
|
90
|
+
return unwrapped;
|
|
91
|
+
if (ts.isIdentifier(expression))
|
|
92
|
+
return bindings.get(expression.text);
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
function unwrapDefineAgent(expression) {
|
|
96
|
+
if (ts.isObjectLiteralExpression(expression))
|
|
97
|
+
return expression;
|
|
98
|
+
if (ts.isCallExpression(expression) &&
|
|
99
|
+
ts.isIdentifier(expression.expression) &&
|
|
100
|
+
expression.expression.text === 'defineAgent' &&
|
|
101
|
+
expression.arguments.length === 1) {
|
|
102
|
+
const arg = expression.arguments[0];
|
|
103
|
+
if (arg && ts.isObjectLiteralExpression(arg))
|
|
104
|
+
return arg;
|
|
105
|
+
}
|
|
106
|
+
if (ts.isAsExpression(expression) ||
|
|
107
|
+
ts.isSatisfiesExpression(expression) ||
|
|
108
|
+
ts.isParenthesizedExpression(expression)) {
|
|
109
|
+
return unwrapDefineAgent(expression.expression);
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
function objectLiteralToValue(literal, fileName) {
|
|
114
|
+
const result = {};
|
|
115
|
+
for (const property of literal.properties) {
|
|
116
|
+
if (ts.isSpreadAssignment(property)) {
|
|
117
|
+
const spread = expressionToValue(property.expression, fileName);
|
|
118
|
+
if (!spread || typeof spread !== 'object' || Array.isArray(spread)) {
|
|
119
|
+
throw new Error(`${fileName}: object spreads must be object literals`);
|
|
120
|
+
}
|
|
121
|
+
Object.assign(result, spread);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
125
|
+
throw new Error(`${fileName}: only data properties are allowed in agent definitions ` +
|
|
126
|
+
`(no methods, getters, or shorthand)`);
|
|
127
|
+
}
|
|
128
|
+
const key = propertyNameText(property.name, fileName);
|
|
129
|
+
result[key] = expressionToValue(property.initializer, fileName);
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
function propertyNameText(name, fileName) {
|
|
134
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
135
|
+
return name.text;
|
|
136
|
+
}
|
|
137
|
+
throw new Error(`${fileName}: computed property names are not allowed`);
|
|
138
|
+
}
|
|
139
|
+
function expressionToValue(expression, fileName) {
|
|
140
|
+
expression = unwrapExpression(expression);
|
|
141
|
+
if (ts.isStringLiteralLike(expression))
|
|
142
|
+
return expression.text;
|
|
143
|
+
if (ts.isNumericLiteral(expression))
|
|
144
|
+
return Number(expression.text);
|
|
145
|
+
if (expression.kind === ts.SyntaxKind.TrueKeyword)
|
|
146
|
+
return true;
|
|
147
|
+
if (expression.kind === ts.SyntaxKind.FalseKeyword)
|
|
148
|
+
return false;
|
|
149
|
+
if (expression.kind === ts.SyntaxKind.NullKeyword)
|
|
150
|
+
return null;
|
|
151
|
+
if (ts.isPrefixUnaryExpression(expression) &&
|
|
152
|
+
expression.operator === ts.SyntaxKind.MinusToken &&
|
|
153
|
+
ts.isNumericLiteral(expression.operand)) {
|
|
154
|
+
return -Number(expression.operand.text);
|
|
155
|
+
}
|
|
156
|
+
if (ts.isObjectLiteralExpression(expression)) {
|
|
157
|
+
return objectLiteralToValue(expression, fileName);
|
|
158
|
+
}
|
|
159
|
+
if (ts.isArrayLiteralExpression(expression)) {
|
|
160
|
+
return expression.elements.flatMap((element) => {
|
|
161
|
+
if (ts.isSpreadElement(element)) {
|
|
162
|
+
const spread = expressionToValue(element.expression, fileName);
|
|
163
|
+
if (!Array.isArray(spread)) {
|
|
164
|
+
throw new Error(`${fileName}: array spreads must be array literals`);
|
|
165
|
+
}
|
|
166
|
+
return spread;
|
|
167
|
+
}
|
|
168
|
+
return [expressionToValue(element, fileName)];
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
throw new Error(`${fileName}: agent definitions must be pure data literals ` +
|
|
172
|
+
`(found ${ts.SyntaxKind[expression.kind]})`);
|
|
173
|
+
}
|
|
174
|
+
function unwrapExpression(expression) {
|
|
175
|
+
while (ts.isAsExpression(expression) ||
|
|
176
|
+
ts.isSatisfiesExpression(expression) ||
|
|
177
|
+
ts.isParenthesizedExpression(expression)) {
|
|
178
|
+
expression = expression.expression;
|
|
179
|
+
}
|
|
180
|
+
return expression;
|
|
181
|
+
}
|
package/dist/yaml.d.ts
ADDED
package/dist/yaml.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { JSON_SCHEMA, load as loadYaml } from 'js-yaml';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { agentDefinitionSchema } from './index.js';
|
|
4
|
+
export const MAX_YAML_BYTES = 1_000_000;
|
|
5
|
+
export function parseYamlAgent({ yamlContent, filename, }) {
|
|
6
|
+
if (Buffer.byteLength(yamlContent, 'utf8') > MAX_YAML_BYTES) {
|
|
7
|
+
throw new Error(`Invalid YAML in ${filename}: content exceeds the maximum size of ${MAX_YAML_BYTES} bytes`);
|
|
8
|
+
}
|
|
9
|
+
const sourceWithStringId = yamlContent.replace(/^(\s*id:\s*)([0-9a-f]{24})(\s*(?:#.*)?)$/m, '$1"$2"$3');
|
|
10
|
+
const parsed = loadYaml(sourceWithStringId, {
|
|
11
|
+
schema: JSON_SCHEMA,
|
|
12
|
+
});
|
|
13
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
14
|
+
throw new Error(`Invalid YAML in ${filename}: expected an object`);
|
|
15
|
+
}
|
|
16
|
+
const input = Object.fromEntries(Object.entries(parsed));
|
|
17
|
+
try {
|
|
18
|
+
return agentDefinitionSchema.parse(input);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error instanceof z.ZodError) {
|
|
22
|
+
const issues = error.issues
|
|
23
|
+
.map((issue) => `${issue.path.join('.') || '(root)'} — ${issue.message}`)
|
|
24
|
+
.join('; ');
|
|
25
|
+
throw new Error(`Invalid YAML in ${filename}: ${issues}`);
|
|
26
|
+
}
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sequenceholdings/agent-spec",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Typed agent definition contract, compiler, hashing, and migration utilities",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/sequenceholdings/studio.git",
|
|
9
|
+
"directory": "shared/services/agent-spec"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./compiler": {
|
|
25
|
+
"types": "./dist/compiler.d.ts",
|
|
26
|
+
"import": "./dist/compiler.js",
|
|
27
|
+
"default": "./dist/compiler.js"
|
|
28
|
+
},
|
|
29
|
+
"./yaml": {
|
|
30
|
+
"types": "./dist/yaml.d.ts",
|
|
31
|
+
"import": "./dist/yaml.js",
|
|
32
|
+
"default": "./dist/yaml.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"js-yaml": "^4.1.1",
|
|
41
|
+
"typescript": "^5.6.0",
|
|
42
|
+
"zod": "^4.1.13"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/js-yaml": "^4.0.9",
|
|
46
|
+
"@types/node": "^22.0.0",
|
|
47
|
+
"vitest": "^4.1.5"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=20"
|
|
51
|
+
},
|
|
52
|
+
"keywords": [
|
|
53
|
+
"agents",
|
|
54
|
+
"spec",
|
|
55
|
+
"zod"
|
|
56
|
+
],
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "rm -rf dist && tsc",
|
|
59
|
+
"type-check": "tsc --noEmit",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"test:package": "pnpm run build && pnpm run type-check && pnpm run test && node scripts/check-package.mjs",
|
|
62
|
+
"test:watch": "vitest"
|
|
63
|
+
}
|
|
64
|
+
}
|