@charming_groot/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/config.d.ts +669 -0
  2. package/dist/config.d.ts.map +1 -0
  3. package/dist/config.js +99 -0
  4. package/dist/config.js.map +1 -0
  5. package/dist/errors/base-error.d.ts +29 -0
  6. package/dist/errors/base-error.d.ts.map +1 -0
  7. package/dist/errors/base-error.js +57 -0
  8. package/dist/errors/base-error.js.map +1 -0
  9. package/dist/errors/index.d.ts +2 -0
  10. package/dist/errors/index.d.ts.map +1 -0
  11. package/dist/errors/index.js +2 -0
  12. package/dist/errors/index.js.map +1 -0
  13. package/dist/event-bus.d.ts +14 -0
  14. package/dist/event-bus.d.ts.map +1 -0
  15. package/dist/event-bus.js +64 -0
  16. package/dist/event-bus.js.map +1 -0
  17. package/dist/index.d.ts +11 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +12 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/logger.d.ts +7 -0
  22. package/dist/logger.d.ts.map +1 -0
  23. package/dist/logger.js +33 -0
  24. package/dist/logger.js.map +1 -0
  25. package/dist/registry.d.ts +15 -0
  26. package/dist/registry.d.ts.map +1 -0
  27. package/dist/registry.js +46 -0
  28. package/dist/registry.js.map +1 -0
  29. package/dist/run-context.d.ts +20 -0
  30. package/dist/run-context.d.ts.map +1 -0
  31. package/dist/run-context.js +39 -0
  32. package/dist/run-context.js.map +1 -0
  33. package/dist/types/auth.d.ts +56 -0
  34. package/dist/types/auth.d.ts.map +1 -0
  35. package/dist/types/auth.js +2 -0
  36. package/dist/types/auth.js.map +1 -0
  37. package/dist/types/common.d.ts +15 -0
  38. package/dist/types/common.d.ts.map +1 -0
  39. package/dist/types/common.js +2 -0
  40. package/dist/types/common.js.map +1 -0
  41. package/dist/types/events.d.ts +77 -0
  42. package/dist/types/events.d.ts.map +1 -0
  43. package/dist/types/events.js +2 -0
  44. package/dist/types/events.js.map +1 -0
  45. package/dist/types/index.d.ts +8 -0
  46. package/dist/types/index.d.ts.map +1 -0
  47. package/dist/types/index.js +2 -0
  48. package/dist/types/index.js.map +1 -0
  49. package/dist/types/provider.d.ts +42 -0
  50. package/dist/types/provider.d.ts.map +1 -0
  51. package/dist/types/provider.js +2 -0
  52. package/dist/types/provider.js.map +1 -0
  53. package/dist/types/sandbox.d.ts +27 -0
  54. package/dist/types/sandbox.d.ts.map +1 -0
  55. package/dist/types/sandbox.js +2 -0
  56. package/dist/types/sandbox.js.map +1 -0
  57. package/dist/types/tool.d.ts +43 -0
  58. package/dist/types/tool.d.ts.map +1 -0
  59. package/dist/types/tool.js +8 -0
  60. package/dist/types/tool.js.map +1 -0
  61. package/package.json +34 -0
  62. package/src/config.ts +119 -0
  63. package/src/errors/base-error.ts +66 -0
  64. package/src/errors/index.ts +10 -0
  65. package/src/event-bus.ts +78 -0
  66. package/src/index.ts +74 -0
  67. package/src/logger.ts +43 -0
  68. package/src/registry.ts +62 -0
  69. package/src/run-context.ts +48 -0
  70. package/src/types/auth.ts +79 -0
  71. package/src/types/common.ts +22 -0
  72. package/src/types/events.ts +24 -0
  73. package/src/types/index.ts +54 -0
  74. package/src/types/provider.ts +50 -0
  75. package/src/types/sandbox.ts +29 -0
  76. package/src/types/tool.ts +37 -0
  77. package/tests/config.test.ts +98 -0
  78. package/tests/errors.test.ts +87 -0
  79. package/tests/event-bus.test.ts +89 -0
  80. package/tests/logger.test.ts +34 -0
  81. package/tests/registry.test.ts +90 -0
  82. package/tests/run-context.test.ts +80 -0
  83. package/tsconfig.json +9 -0
  84. package/vitest.config.ts +9 -0
package/src/logger.ts ADDED
@@ -0,0 +1,43 @@
1
+ import pino from 'pino';
2
+
3
+ export type AgentLogger = pino.Logger;
4
+ export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
5
+
6
+ const LOG_LEVEL_KEY = 'CLI_AGENT_LOG_LEVEL';
7
+ const DEFAULT_LOG_LEVEL: LogLevel = 'info';
8
+
9
+ function getLogLevel(): LogLevel {
10
+ const envLevel = process.env[LOG_LEVEL_KEY];
11
+ if (envLevel && isValidLogLevel(envLevel)) {
12
+ return envLevel;
13
+ }
14
+ return DEFAULT_LOG_LEVEL;
15
+ }
16
+
17
+ function isValidLogLevel(level: string): level is LogLevel {
18
+ return ['trace', 'debug', 'info', 'warn', 'error', 'fatal'].includes(level);
19
+ }
20
+
21
+ export function createLogger(name: string, level?: LogLevel): pino.Logger {
22
+ return pino({
23
+ name,
24
+ level: level ?? getLogLevel(),
25
+ transport:
26
+ process.env['NODE_ENV'] !== 'production'
27
+ ? { target: 'pino/file', options: { destination: 1 } }
28
+ : undefined,
29
+ });
30
+ }
31
+
32
+ let rootLogger: pino.Logger | undefined;
33
+
34
+ export function getRootLogger(): pino.Logger {
35
+ if (!rootLogger) {
36
+ rootLogger = createLogger('cli-agent');
37
+ }
38
+ return rootLogger;
39
+ }
40
+
41
+ export function createChildLogger(name: string): pino.Logger {
42
+ return getRootLogger().child({ module: name });
43
+ }
@@ -0,0 +1,62 @@
1
+ import { RegistryError } from './errors/base-error.js';
2
+
3
+ export class Registry<T> {
4
+ private readonly items = new Map<string, T>();
5
+ private readonly label: string;
6
+
7
+ constructor(label: string) {
8
+ this.label = label;
9
+ }
10
+
11
+ register(name: string, item: T): void {
12
+ if (this.items.has(name)) {
13
+ throw new RegistryError(
14
+ `${this.label} '${name}' is already registered`
15
+ );
16
+ }
17
+ this.items.set(name, item);
18
+ }
19
+
20
+ get(name: string): T {
21
+ const item = this.items.get(name);
22
+ if (item === undefined) {
23
+ throw new RegistryError(
24
+ `${this.label} '${name}' is not registered`
25
+ );
26
+ }
27
+ return item;
28
+ }
29
+
30
+ tryGet(name: string): T | undefined {
31
+ return this.items.get(name);
32
+ }
33
+
34
+ has(name: string): boolean {
35
+ return this.items.has(name);
36
+ }
37
+
38
+ getAll(): ReadonlyMap<string, T> {
39
+ return this.items;
40
+ }
41
+
42
+ getAllNames(): readonly string[] {
43
+ return [...this.items.keys()];
44
+ }
45
+
46
+ unregister(name: string): boolean {
47
+ if (!this.items.has(name)) {
48
+ throw new RegistryError(
49
+ `${this.label} '${name}' is not registered`
50
+ );
51
+ }
52
+ return this.items.delete(name);
53
+ }
54
+
55
+ clear(): void {
56
+ this.items.clear();
57
+ }
58
+
59
+ get size(): number {
60
+ return this.items.size;
61
+ }
62
+ }
@@ -0,0 +1,48 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { AgentConfig } from './config.js';
3
+ import { EventBus } from './event-bus.js';
4
+ import type { JsonValue } from './types/common.js';
5
+
6
+ export class RunContext {
7
+ readonly runId: string;
8
+ readonly workingDirectory: string;
9
+ readonly eventBus: EventBus;
10
+ readonly config: AgentConfig;
11
+ readonly createdAt: Date;
12
+ private readonly metadata: Map<string, JsonValue>;
13
+ private abortController: AbortController;
14
+
15
+ constructor(config: AgentConfig, eventBus?: EventBus) {
16
+ this.runId = randomUUID();
17
+ this.workingDirectory = config.workingDirectory;
18
+ this.config = config;
19
+ this.eventBus = eventBus ?? new EventBus();
20
+ this.createdAt = new Date();
21
+ this.metadata = new Map();
22
+ this.abortController = new AbortController();
23
+ }
24
+
25
+ get signal(): AbortSignal {
26
+ return this.abortController.signal;
27
+ }
28
+
29
+ get isAborted(): boolean {
30
+ return this.abortController.signal.aborted;
31
+ }
32
+
33
+ abort(reason?: string): void {
34
+ this.abortController.abort(reason);
35
+ }
36
+
37
+ setMetadata(key: string, value: JsonValue): void {
38
+ this.metadata.set(key, value);
39
+ }
40
+
41
+ getMetadata(key: string): JsonValue | undefined {
42
+ return this.metadata.get(key);
43
+ }
44
+
45
+ getAllMetadata(): ReadonlyMap<string, JsonValue> {
46
+ return this.metadata;
47
+ }
48
+ }
@@ -0,0 +1,79 @@
1
+ export type AuthType =
2
+ | 'no-auth'
3
+ | 'api-key'
4
+ | 'oauth'
5
+ | 'azure-ad'
6
+ | 'aws-iam'
7
+ | 'gcp-service-account'
8
+ | 'credential-file';
9
+
10
+ export interface NoAuth {
11
+ readonly type: 'no-auth';
12
+ }
13
+
14
+ export interface ApiKeyAuth {
15
+ readonly type: 'api-key';
16
+ readonly apiKey: string;
17
+ }
18
+
19
+ export interface OAuthAuth {
20
+ readonly type: 'oauth';
21
+ readonly clientId: string;
22
+ readonly clientSecret: string;
23
+ readonly tokenUrl: string;
24
+ readonly scopes?: readonly string[];
25
+ readonly accessToken?: string;
26
+ readonly refreshToken?: string;
27
+ }
28
+
29
+ export interface AzureAdAuth {
30
+ readonly type: 'azure-ad';
31
+ readonly tenantId: string;
32
+ readonly clientId: string;
33
+ readonly clientSecret?: string;
34
+ readonly accessToken?: string;
35
+ }
36
+
37
+ export interface AwsIamAuth {
38
+ readonly type: 'aws-iam';
39
+ readonly accessKeyId?: string;
40
+ readonly secretAccessKey?: string;
41
+ readonly sessionToken?: string;
42
+ readonly region: string;
43
+ readonly profile?: string;
44
+ }
45
+
46
+ export interface GcpServiceAccountAuth {
47
+ readonly type: 'gcp-service-account';
48
+ readonly projectId: string;
49
+ readonly keyFilePath?: string;
50
+ readonly accessToken?: string;
51
+ }
52
+
53
+ export interface CredentialFileAuth {
54
+ readonly type: 'credential-file';
55
+ readonly filePath: string;
56
+ readonly profile?: string;
57
+ }
58
+
59
+ export type AuthConfig =
60
+ | NoAuth
61
+ | ApiKeyAuth
62
+ | OAuthAuth
63
+ | AzureAdAuth
64
+ | AwsIamAuth
65
+ | GcpServiceAccountAuth
66
+ | CredentialFileAuth;
67
+
68
+ export interface IAuthStrategy {
69
+ readonly type: AuthType;
70
+ resolve(config: AuthConfig): Promise<ResolvedCredential>;
71
+ refresh?(config: AuthConfig): Promise<ResolvedCredential>;
72
+ }
73
+
74
+ export interface ResolvedCredential {
75
+ readonly type: AuthType;
76
+ readonly headers: Record<string, string>;
77
+ readonly token?: string;
78
+ readonly expiresAt?: Date;
79
+ }
@@ -0,0 +1,22 @@
1
+ export type JsonValue =
2
+ | string
3
+ | number
4
+ | boolean
5
+ | null
6
+ | JsonValue[]
7
+ | { [key: string]: JsonValue };
8
+
9
+ export type JsonObject = Record<string, JsonValue>;
10
+
11
+ export interface Identifiable {
12
+ readonly id: string;
13
+ }
14
+
15
+ export interface Timestamped {
16
+ readonly createdAt: Date;
17
+ readonly updatedAt: Date;
18
+ }
19
+
20
+ export interface Disposable {
21
+ dispose(): Promise<void>;
22
+ }
@@ -0,0 +1,24 @@
1
+ import type { Message, LlmResponse, ToolCall } from './provider.js';
2
+ import type { ToolResult } from './tool.js';
3
+ import type { ExecutionResult } from './sandbox.js';
4
+
5
+ export interface AgentEvents {
6
+ 'agent:start': { runId: string; model: string; startedAt: number };
7
+ 'agent:end': { runId: string; reason: string; durationMs: number; iterations: number };
8
+ 'agent:error': { runId: string; error: Error };
9
+ 'llm:request': { runId: string; messages: readonly Message[] };
10
+ 'llm:response': { runId: string; response: LlmResponse; durationMs: number; model: string };
11
+ 'llm:stream': { runId: string; chunk: string };
12
+ 'tool:start': { runId: string; toolCall: ToolCall; startedAt: number };
13
+ 'tool:end': { runId: string; toolCall: ToolCall; result: ToolResult; durationMs: number };
14
+ 'tool:permission': { runId: string; toolName: string };
15
+ 'sandbox:execute': { runId: string; language: string };
16
+ 'sandbox:result': { runId: string; result: ExecutionResult };
17
+ 'mcp:connected': { server: string; toolCount: number };
18
+ 'mcp:disconnected': { server: string };
19
+ 'mcp:tools_changed': { server: string; tools: readonly string[] };
20
+ 'context:assembled': { runId: string; usage: unknown; wasCompressed: boolean; toolCount: number };
21
+ }
22
+
23
+ export type EventName = keyof AgentEvents;
24
+ export type EventPayload<K extends EventName> = AgentEvents[K];
@@ -0,0 +1,54 @@
1
+ export type {
2
+ JsonValue,
3
+ JsonObject,
4
+ Identifiable,
5
+ Timestamped,
6
+ Disposable,
7
+ } from './common.js';
8
+
9
+ export type {
10
+ ToolParameter,
11
+ ToolDescription,
12
+ ToolResult,
13
+ ITool,
14
+ } from './tool.js';
15
+ export { toolResultSchema } from './tool.js';
16
+
17
+ export type {
18
+ MessageRole,
19
+ ToolCall,
20
+ ToolResultMessage,
21
+ Message,
22
+ StopReason,
23
+ LlmResponse,
24
+ TokenUsage,
25
+ StreamEvent,
26
+ ILlmProvider,
27
+ } from './provider.js';
28
+
29
+ export type {
30
+ SandboxConfig,
31
+ ExecutionRequest,
32
+ ExecutionResult,
33
+ ISandbox,
34
+ } from './sandbox.js';
35
+
36
+ export type {
37
+ AgentEvents,
38
+ EventName,
39
+ EventPayload,
40
+ } from './events.js';
41
+
42
+ export type {
43
+ AuthType,
44
+ NoAuth,
45
+ ApiKeyAuth,
46
+ OAuthAuth,
47
+ AzureAdAuth,
48
+ AwsIamAuth,
49
+ GcpServiceAccountAuth,
50
+ CredentialFileAuth,
51
+ AuthConfig,
52
+ IAuthStrategy,
53
+ ResolvedCredential,
54
+ } from './auth.js';
@@ -0,0 +1,50 @@
1
+ import type { ToolDescription } from './tool.js';
2
+
3
+ export type MessageRole = 'user' | 'assistant' | 'system';
4
+
5
+ export interface ToolCall {
6
+ readonly id: string;
7
+ readonly name: string;
8
+ readonly arguments: string;
9
+ }
10
+
11
+ export interface ToolResultMessage {
12
+ readonly toolCallId: string;
13
+ readonly content: string;
14
+ }
15
+
16
+ export interface Message {
17
+ readonly role: MessageRole;
18
+ readonly content: string;
19
+ readonly toolCalls?: readonly ToolCall[];
20
+ readonly toolResults?: readonly ToolResultMessage[];
21
+ }
22
+
23
+ export type StopReason = 'end_turn' | 'tool_use' | 'max_tokens' | 'error';
24
+
25
+ export interface LlmResponse {
26
+ readonly content: string;
27
+ readonly stopReason: StopReason;
28
+ readonly toolCalls: readonly ToolCall[];
29
+ readonly usage: TokenUsage;
30
+ }
31
+
32
+ export interface TokenUsage {
33
+ readonly inputTokens: number;
34
+ readonly outputTokens: number;
35
+ /** Time spent in extended thinking / <think> block (ms). Only set by providers that support it. */
36
+ readonly thinkingMs?: number;
37
+ }
38
+
39
+ export interface StreamEvent {
40
+ readonly type: 'text_delta' | 'tool_call_start' | 'tool_call_delta' | 'done';
41
+ readonly content?: string;
42
+ readonly toolCall?: Partial<ToolCall>;
43
+ readonly response?: LlmResponse;
44
+ }
45
+
46
+ export interface ILlmProvider {
47
+ readonly providerId: string;
48
+ chat(messages: readonly Message[], tools?: readonly ToolDescription[]): Promise<LlmResponse>;
49
+ stream(messages: readonly Message[], tools?: readonly ToolDescription[]): AsyncIterable<StreamEvent>;
50
+ }
@@ -0,0 +1,29 @@
1
+ export interface SandboxConfig {
2
+ readonly image: string;
3
+ readonly memoryLimitMb: number;
4
+ readonly cpuLimit: number;
5
+ readonly timeoutMs: number;
6
+ readonly workDir: string;
7
+ }
8
+
9
+ export interface ExecutionRequest {
10
+ readonly code: string;
11
+ readonly language: string;
12
+ readonly timeoutMs?: number;
13
+ readonly stdin?: string;
14
+ }
15
+
16
+ export interface ExecutionResult {
17
+ readonly exitCode: number;
18
+ readonly stdout: string;
19
+ readonly stderr: string;
20
+ readonly timedOut: boolean;
21
+ readonly durationMs: number;
22
+ }
23
+
24
+ export interface ISandbox {
25
+ readonly containerId: string;
26
+ initialize(config: SandboxConfig): Promise<void>;
27
+ execute(request: ExecutionRequest): Promise<ExecutionResult>;
28
+ destroy(): Promise<void>;
29
+ }
@@ -0,0 +1,37 @@
1
+ import { z } from 'zod';
2
+ import type { JsonObject } from './common.js';
3
+ import type { RunContext } from '../run-context.js';
4
+
5
+ export interface ToolParameter {
6
+ readonly name: string;
7
+ readonly type: string;
8
+ readonly description: string;
9
+ readonly required: boolean;
10
+ }
11
+
12
+ export interface ToolDescription {
13
+ readonly name: string;
14
+ readonly description: string;
15
+ readonly parameters: readonly ToolParameter[];
16
+ }
17
+
18
+ export interface ToolResult {
19
+ readonly success: boolean;
20
+ readonly output: string;
21
+ readonly error?: string;
22
+ readonly metadata?: JsonObject;
23
+ }
24
+
25
+ export interface ITool {
26
+ readonly name: string;
27
+ readonly requiresPermission: boolean;
28
+ describe(): ToolDescription;
29
+ execute(params: JsonObject, context: RunContext): Promise<ToolResult>;
30
+ }
31
+
32
+ export const toolResultSchema = z.object({
33
+ success: z.boolean(),
34
+ output: z.string(),
35
+ error: z.string().optional(),
36
+ metadata: z.record(z.unknown()).optional(),
37
+ });
@@ -0,0 +1,98 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ parseConfig,
4
+ parseAgentConfig,
5
+ providerConfigSchema,
6
+ agentConfigSchema,
7
+ } from '../src/config.js';
8
+ import { ConfigError } from '../src/errors/base-error.js';
9
+
10
+ const VALID_PROVIDER = {
11
+ providerId: 'claude',
12
+ model: 'claude-opus-4-6',
13
+ auth: { type: 'api-key' as const, apiKey: 'sk-test-key-123' },
14
+ };
15
+
16
+ describe('Config', () => {
17
+ describe('providerConfigSchema', () => {
18
+ it('should validate a valid provider config', () => {
19
+ const result = providerConfigSchema.safeParse(VALID_PROVIDER);
20
+ expect(result.success).toBe(true);
21
+ if (result.success) {
22
+ expect(result.data.maxTokens).toBe(4096);
23
+ expect(result.data.temperature).toBe(0.7);
24
+ }
25
+ });
26
+
27
+ it('should reject empty providerId', () => {
28
+ const result = providerConfigSchema.safeParse({
29
+ ...VALID_PROVIDER,
30
+ providerId: '',
31
+ });
32
+ expect(result.success).toBe(false);
33
+ });
34
+
35
+ it('should reject invalid baseUrl', () => {
36
+ const result = providerConfigSchema.safeParse({
37
+ ...VALID_PROVIDER,
38
+ baseUrl: 'not-a-url',
39
+ });
40
+ expect(result.success).toBe(false);
41
+ });
42
+
43
+ it('should accept custom maxTokens and temperature', () => {
44
+ const result = providerConfigSchema.safeParse({
45
+ ...VALID_PROVIDER,
46
+ maxTokens: 8192,
47
+ temperature: 1.0,
48
+ });
49
+ expect(result.success).toBe(true);
50
+ if (result.success) {
51
+ expect(result.data.maxTokens).toBe(8192);
52
+ expect(result.data.temperature).toBe(1.0);
53
+ }
54
+ });
55
+ });
56
+
57
+ describe('parseConfig', () => {
58
+ it('should parse valid config', () => {
59
+ const config = parseConfig(providerConfigSchema, VALID_PROVIDER);
60
+ expect(config.providerId).toBe('claude');
61
+ });
62
+
63
+ it('should throw ConfigError on invalid config', () => {
64
+ expect(() => parseConfig(providerConfigSchema, {})).toThrow(ConfigError);
65
+ });
66
+
67
+ it('should include field paths in error message', () => {
68
+ try {
69
+ parseConfig(providerConfigSchema, {});
70
+ } catch (e) {
71
+ expect(e).toBeInstanceOf(ConfigError);
72
+ expect((e as ConfigError).message).toContain('providerId');
73
+ }
74
+ });
75
+ });
76
+
77
+ describe('parseAgentConfig', () => {
78
+ it('should parse a valid agent config', () => {
79
+ const config = parseAgentConfig({
80
+ provider: VALID_PROVIDER,
81
+ });
82
+ expect(config.provider.providerId).toBe('claude');
83
+ expect(config.maxIterations).toBe(50);
84
+ });
85
+
86
+ it('should apply defaults', () => {
87
+ const config = parseAgentConfig({
88
+ provider: VALID_PROVIDER,
89
+ });
90
+ expect(config.maxIterations).toBe(50);
91
+ expect(config.workingDirectory).toBeTruthy();
92
+ });
93
+
94
+ it('should throw on missing provider', () => {
95
+ expect(() => parseAgentConfig({})).toThrow(ConfigError);
96
+ });
97
+ });
98
+ });
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ AgentError,
4
+ RegistryError,
5
+ ConfigError,
6
+ ProviderError,
7
+ ToolExecutionError,
8
+ SandboxError,
9
+ PermissionDeniedError,
10
+ AbortError,
11
+ } from '../src/errors/base-error.js';
12
+
13
+ describe('Errors', () => {
14
+ it('should create AgentError with code', () => {
15
+ const err = new AgentError('test', 'TEST_CODE');
16
+ expect(err.message).toBe('test');
17
+ expect(err.code).toBe('TEST_CODE');
18
+ expect(err.name).toBe('AgentError');
19
+ expect(err).toBeInstanceOf(Error);
20
+ });
21
+
22
+ it('should chain cause', () => {
23
+ const cause = new Error('original');
24
+ const err = new AgentError('wrapped', 'WRAP', cause);
25
+ expect(err.cause).toBe(cause);
26
+ });
27
+
28
+ it('should create RegistryError', () => {
29
+ const err = new RegistryError('not found');
30
+ expect(err.code).toBe('REGISTRY_ERROR');
31
+ expect(err.name).toBe('RegistryError');
32
+ expect(err).toBeInstanceOf(AgentError);
33
+ });
34
+
35
+ it('should create ConfigError', () => {
36
+ const err = new ConfigError('bad config');
37
+ expect(err.code).toBe('CONFIG_ERROR');
38
+ expect(err.name).toBe('ConfigError');
39
+ });
40
+
41
+ it('should create ProviderError', () => {
42
+ const err = new ProviderError('api failed');
43
+ expect(err.code).toBe('PROVIDER_ERROR');
44
+ expect(err.name).toBe('ProviderError');
45
+ });
46
+
47
+ it('should create ToolExecutionError with toolName', () => {
48
+ const err = new ToolExecutionError('file-read', 'read failed');
49
+ expect(err.toolName).toBe('file-read');
50
+ expect(err.code).toBe('TOOL_EXECUTION_ERROR');
51
+ expect(err.name).toBe('ToolExecutionError');
52
+ });
53
+
54
+ it('should create SandboxError', () => {
55
+ const err = new SandboxError('container failed');
56
+ expect(err.code).toBe('SANDBOX_ERROR');
57
+ expect(err.name).toBe('SandboxError');
58
+ });
59
+
60
+ it('should create PermissionDeniedError', () => {
61
+ const err = new PermissionDeniedError('shell-exec');
62
+ expect(err.toolName).toBe('shell-exec');
63
+ expect(err.code).toBe('PERMISSION_DENIED');
64
+ expect(err.message).toContain('shell-exec');
65
+ });
66
+
67
+ it('should create AbortError with default message', () => {
68
+ const err = new AbortError();
69
+ expect(err.message).toBe('Operation aborted');
70
+ expect(err.code).toBe('ABORT_ERROR');
71
+ });
72
+
73
+ it('should create AbortError with custom message', () => {
74
+ const err = new AbortError('user cancelled');
75
+ expect(err.message).toBe('user cancelled');
76
+ });
77
+
78
+ it('should be catchable as Error', () => {
79
+ try {
80
+ throw new RegistryError('test');
81
+ } catch (e) {
82
+ expect(e).toBeInstanceOf(Error);
83
+ expect(e).toBeInstanceOf(AgentError);
84
+ expect(e).toBeInstanceOf(RegistryError);
85
+ }
86
+ });
87
+ });