Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 80 additions & 40 deletions src/function.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
export enum ExecutableGameFunctionStatus {
Done = "done",
Failed = "failed",
Done = 'done',
Failed = 'failed',
InProgress = 'in_progress'
}

export type ExecutableGameFunctionResponseJSON = ReturnType<
ExecutableGameFunctionResponse["toJSON"]
>;
export interface ExecutableGameFunctionResponseJSON {
action_id: string;
action_status: ExecutableGameFunctionStatus;
feedback_message: string;
}

export class ExecutableGameFunctionResponse {
constructor(
public status: ExecutableGameFunctionStatus,
public feedback: string
) {}

toJSON(id: string) {
toJSON(id: string): ExecutableGameFunctionResponseJSON {
return {
action_id: id,
action_status: this.status,
Expand All @@ -22,63 +25,76 @@ export class ExecutableGameFunctionResponse {
}
}

interface IGameFunction<T extends GameFunctionArg[]> {
name: string;
description: string;
args: T;
executable: (
args: Partial<ExecutableArgs<T>>,
logger: (msg: string) => void
) => Promise<ExecutableGameFunctionResponse>;
hint?: string;
}

export interface GameFunctionArg {
name: string;
description: string;
type?: string;
optional?: boolean;
defaultValue?: string;
validator?: (value: string) => boolean;
}

export type GameFunctionBase = {
export interface IGameFunction<T extends GameFunctionArg[]> {
name: string;
description: string;
args: GameFunctionArg[];
args: T;
executable: (
args: Record<string, string>,
args: Partial<ExecutableArgs<T>>,
logger: (msg: string) => void
) => Promise<ExecutableGameFunctionResponse>;
hint?: string;
execute: (
args: Record<string, { value: string }>,
logger: (msg: string) => void
) => Promise<ExecutableGameFunctionResponse>;
toJSON(): Object;
};
}

type ExecutableArgs<T extends GameFunctionArg[]> = {
[K in T[number]["name"]]: string;
export type ExecutableArgs<T extends GameFunctionArg[]> = {
[K in T[number]['name']]: string;
};

class GameFunction<T extends GameFunctionArg[]> implements IGameFunction<T> {
public name: string;
public description: string;
public args: T;
public executable: (
public readonly name: string;
public readonly description: string;
public readonly args: T;
public readonly executable: (
args: Partial<ExecutableArgs<T>>,
logger: (msg: string) => void
) => Promise<ExecutableGameFunctionResponse>;
public hint?: string;
public readonly hint?: string;

constructor(options: IGameFunction<T>) {
this.name = options.name;
this.description = options.description;
this.args = options.args;
this.executable = options.executable;
this.hint = options.hint;

// Validate the function configuration
this.validateConfiguration();
}

toJSON() {
private validateConfiguration(): void {
if (!this.name || this.name.trim() === '') {
throw new Error('Function name is required');
}

if (!this.description || this.description.trim() === '') {
throw new Error('Function description is required');
}

if (!Array.isArray(this.args)) {
throw new Error('Function args must be an array');
}

this.args.forEach((arg, index) => {
if (!arg.name || arg.name.trim() === '') {
throw new Error(`Argument at index ${index} must have a name`);
}

if (!arg.description || arg.description.trim() === '') {
throw new Error(`Argument ${arg.name} must have a description`);
}
});
}

toJSON(): Record<string, unknown> {
return {
fn_name: this.name,
fn_description: this.description,
Expand All @@ -87,21 +103,45 @@ class GameFunction<T extends GameFunctionArg[]> implements IGameFunction<T> {
};
}

private validateArgs(args: Record<string, { value: string }>): void {
this.args.forEach((arg) => {
const value = args[arg.name]?.value;

if (!arg.optional && (value === undefined || value === null || value === '')) {
throw new Error(`Required argument ${arg.name} is missing`);
}

if (value && arg.validator && !arg.validator(value)) {
throw new Error(`Invalid value for argument ${arg.name}`);
}
});
}

async execute(
args: {
[key in GameFunctionArg["name"]]: { value: string };
},
args: Record<string, { value: string }>,
logger: (msg: string) => void
) {
const argValues: ExecutableArgs<T> = Object.keys(args).reduce(
): Promise<ExecutableGameFunctionResponse> {
// Validate input arguments
this.validateArgs(args);

// Convert args to expected format
const argValues = Object.keys(args).reduce(
(acc, key) => {
acc[key as keyof ExecutableArgs<T>] = args[key]?.value;
return acc;
},
{} as ExecutableArgs<T>
);

return await this.executable(argValues, logger);
try {
return await this.executable(argValues, logger);
} catch (error) {
logger(`Error executing function ${this.name}: ${error.message}`);
return new ExecutableGameFunctionResponse(
ExecutableGameFunctionStatus.Failed,
`Function execution failed: ${error.message}`
);
}
}
}

Expand Down
64 changes: 64 additions & 0 deletions tests/agent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, expect, it, jest } from '@jest/globals';
import GameAgent from '../src/agent';
import GameWorker from '../src/worker';
import { ExecutableGameFunctionResponse, ExecutableGameFunctionStatus } from '../src/function';
import { ActionType } from '../src/api';
import { AgentNotInitializedError, WorkerNotFoundError } from '../src/errors';

describe('GameAgent', () => {
const mockApiKey = 'test-api-key';
const mockWorker = new GameWorker({
id: 'worker1',
name: 'Test Worker',
description: 'Test worker description',
functions: []
});

const mockAgentConfig = {
name: 'Test Agent',
goal: 'Test Goal',
description: 'Test Description',
workers: [mockWorker]
};

it('should create agent instance correctly', () => {
const agent = new GameAgent(mockApiKey, mockAgentConfig);
expect(agent).toBeInstanceOf(GameAgent);
});

it('should throw error when not initialized', async () => {
const agent = new GameAgent(mockApiKey, mockAgentConfig);
await expect(agent.step()).rejects.toThrow(AgentNotInitializedError);
});

it('should throw error for invalid worker', () => {
const agent = new GameAgent(mockApiKey, mockAgentConfig);
// @ts-ignore - Accessing private method for testing
expect(() => agent.getWorkerById('invalid-id')).toThrow(WorkerNotFoundError);
});

it('should handle initialization correctly', async () => {
const agent = new GameAgent(mockApiKey, mockAgentConfig);
const mockMap = { id: 'map1' };
const mockAgentResponse = { id: 'agent1' };

// Mock the GameClient methods
agent['gameClient'].createMap = jest.fn().mockResolvedValue(mockMap);
agent['gameClient'].createAgent = jest.fn().mockResolvedValue(mockAgentResponse);

await agent.init();

expect(agent['state'].mapId).toBe(mockMap.id);
expect(agent['state'].agentId).toBe(mockAgentResponse.id);
});

it('should handle cleanup correctly', async () => {
const agent = new GameAgent(mockApiKey, mockAgentConfig);
await agent.cleanup();

expect(agent['state'].agentId).toBeNull();
expect(agent['state'].mapId).toBeNull();
expect(agent['state'].currentWorkerId).toBe(mockWorker.id);
expect(agent['state'].lastActionResult).toBeNull();
});
});
88 changes: 88 additions & 0 deletions tests/function.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { describe, expect, it, jest } from '@jest/globals';
import GameFunction, {
ExecutableGameFunctionResponse,
ExecutableGameFunctionStatus,
GameFunctionArg
} from '../src/function';

describe('GameFunction', () => {
const mockArgs: GameFunctionArg[] = [
{
name: 'arg1',
description: 'Test argument 1',
optional: false
}
];

const mockExecutable = async (args: any, logger: any) => {
return new ExecutableGameFunctionResponse(ExecutableGameFunctionStatus.Done, 'Success');
};

const createMockFunction = () => {
return new GameFunction({
name: 'testFunction',
description: 'Test function description',
args: mockArgs,
executable: mockExecutable
});
};

it('should create function instance correctly', () => {
const fn = createMockFunction();
expect(fn).toBeInstanceOf(GameFunction);
});

it('should validate configuration on creation', () => {
expect(() => {
new GameFunction({
name: '',
description: 'Test',
args: mockArgs,
executable: mockExecutable
});
}).toThrow('Function name is required');
});

it('should validate required arguments', async () => {
const fn = createMockFunction();
const mockLogger = jest.fn();

await expect(
fn.execute({}, mockLogger)
).rejects.toThrow('Required argument arg1 is missing');
});

it('should execute successfully with valid arguments', async () => {
const fn = createMockFunction();
const mockLogger = jest.fn();

const result = await fn.execute(
{ arg1: { value: 'test' } },
mockLogger
);

expect(result).toBeInstanceOf(ExecutableGameFunctionResponse);
expect(result.status).toBe(ExecutableGameFunctionStatus.Done);
});

it('should handle execution errors gracefully', async () => {
const errorFn = new GameFunction({
name: 'errorFunction',
description: 'Test error function',
args: mockArgs,
executable: async () => {
throw new Error('Test error');
}
});

const mockLogger = jest.fn();
const result = await errorFn.execute(
{ arg1: { value: 'test' } },
mockLogger
);

expect(result.status).toBe(ExecutableGameFunctionStatus.Failed);
expect(result.feedback).toContain('Test error');
expect(mockLogger).toHaveBeenCalled();
});
});