@cephalization/phoenix-insight 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.
- package/LICENSE +201 -0
- package/README.md +620 -0
- package/dist/agent/index.js +230 -0
- package/dist/cli.js +640 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/px-fetch-more-spans.js +98 -0
- package/dist/commands/px-fetch-more-trace.js +110 -0
- package/dist/config/index.js +165 -0
- package/dist/config/loader.js +141 -0
- package/dist/config/schema.js +53 -0
- package/dist/index.js +1 -0
- package/dist/modes/index.js +17 -0
- package/dist/modes/local.js +134 -0
- package/dist/modes/sandbox.js +121 -0
- package/dist/modes/types.js +1 -0
- package/dist/observability/index.js +65 -0
- package/dist/progress.js +209 -0
- package/dist/prompts/index.js +1 -0
- package/dist/prompts/system.js +30 -0
- package/dist/snapshot/client.js +74 -0
- package/dist/snapshot/context.js +332 -0
- package/dist/snapshot/datasets.js +68 -0
- package/dist/snapshot/experiments.js +135 -0
- package/dist/snapshot/index.js +262 -0
- package/dist/snapshot/projects.js +44 -0
- package/dist/snapshot/prompts.js +199 -0
- package/dist/snapshot/spans.js +80 -0
- package/dist/tsconfig.esm.tsbuildinfo +1 -0
- package/package.json +75 -0
- package/src/agent/index.ts +323 -0
- package/src/cli.ts +782 -0
- package/src/commands/index.ts +8 -0
- package/src/commands/px-fetch-more-spans.ts +174 -0
- package/src/commands/px-fetch-more-trace.ts +183 -0
- package/src/config/index.ts +225 -0
- package/src/config/loader.ts +173 -0
- package/src/config/schema.ts +66 -0
- package/src/index.ts +1 -0
- package/src/modes/index.ts +21 -0
- package/src/modes/local.ts +163 -0
- package/src/modes/sandbox.ts +144 -0
- package/src/modes/types.ts +31 -0
- package/src/observability/index.ts +90 -0
- package/src/progress.ts +239 -0
- package/src/prompts/index.ts +1 -0
- package/src/prompts/system.ts +31 -0
- package/src/snapshot/client.ts +129 -0
- package/src/snapshot/context.ts +462 -0
- package/src/snapshot/datasets.ts +132 -0
- package/src/snapshot/experiments.ts +246 -0
- package/src/snapshot/index.ts +403 -0
- package/src/snapshot/projects.ts +58 -0
- package/src/snapshot/prompts.ts +267 -0
- package/src/snapshot/spans.ts +142 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import { configSchema, getDefaultConfig, type Config } from "./schema.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Default config directory and file path
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_CONFIG_DIR = path.join(os.homedir(), ".phoenix-insight");
|
|
10
|
+
const DEFAULT_CONFIG_FILE = path.join(DEFAULT_CONFIG_DIR, "config.json");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Module-level storage for CLI args passed from commander
|
|
14
|
+
* This is set externally before getConfigPath is called
|
|
15
|
+
*/
|
|
16
|
+
let cliConfigPath: string | undefined;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Set the CLI config path (called from CLI parsing)
|
|
20
|
+
*/
|
|
21
|
+
export function setCliConfigPath(configPath: string | undefined): void {
|
|
22
|
+
cliConfigPath = configPath;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the config file path based on priority:
|
|
27
|
+
* 1. CLI argument (--config)
|
|
28
|
+
* 2. Environment variable (PHOENIX_INSIGHT_CONFIG)
|
|
29
|
+
* 3. Default path (~/.phoenix-insight/config.json)
|
|
30
|
+
*
|
|
31
|
+
* @returns The path to the config file and whether it's the default path
|
|
32
|
+
*/
|
|
33
|
+
export function getConfigPath(): { path: string; isDefault: boolean } {
|
|
34
|
+
// Priority 1: CLI argument
|
|
35
|
+
if (cliConfigPath) {
|
|
36
|
+
return { path: cliConfigPath, isDefault: false };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Priority 2: Environment variable
|
|
40
|
+
const envConfigPath = process.env.PHOENIX_INSIGHT_CONFIG;
|
|
41
|
+
if (envConfigPath) {
|
|
42
|
+
return { path: envConfigPath, isDefault: false };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Priority 3: Default path
|
|
46
|
+
return { path: DEFAULT_CONFIG_FILE, isDefault: true };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Load and parse a config file from disk
|
|
51
|
+
*
|
|
52
|
+
* @param configPath - Path to the config file
|
|
53
|
+
* @returns Parsed JSON object or null if file not found
|
|
54
|
+
* @throws Error if file exists but cannot be parsed as JSON
|
|
55
|
+
*/
|
|
56
|
+
export async function loadConfigFile(
|
|
57
|
+
configPath: string
|
|
58
|
+
): Promise<Record<string, unknown> | null> {
|
|
59
|
+
try {
|
|
60
|
+
const content = await fs.readFile(configPath, "utf-8");
|
|
61
|
+
return JSON.parse(content) as Record<string, unknown>;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// File not found is expected - return null
|
|
64
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// JSON parse errors should be reported
|
|
69
|
+
if (error instanceof SyntaxError) {
|
|
70
|
+
console.warn(
|
|
71
|
+
`Warning: Config file at ${configPath} contains invalid JSON: ${error.message}`
|
|
72
|
+
);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Other errors (permissions, etc.) - warn and return null
|
|
77
|
+
console.warn(
|
|
78
|
+
`Warning: Could not read config file at ${configPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
79
|
+
);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Validate a raw config object against the schema
|
|
86
|
+
* Returns validated config or defaults if validation fails
|
|
87
|
+
*
|
|
88
|
+
* @param raw - Raw config object (or null/undefined)
|
|
89
|
+
* @returns Validated config with defaults applied
|
|
90
|
+
*/
|
|
91
|
+
export function validateConfig(
|
|
92
|
+
raw: Record<string, unknown> | null | undefined
|
|
93
|
+
): Config {
|
|
94
|
+
// If no raw config, return defaults
|
|
95
|
+
if (!raw) {
|
|
96
|
+
return getDefaultConfig();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
// Parse with Zod schema - this applies defaults for missing fields
|
|
101
|
+
return configSchema.parse(raw);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
// Log validation errors as warnings
|
|
104
|
+
if (error instanceof Error && "issues" in error) {
|
|
105
|
+
// Zod error with issues array
|
|
106
|
+
const zodError = error as {
|
|
107
|
+
issues: Array<{ path: string[]; message: string }>;
|
|
108
|
+
};
|
|
109
|
+
zodError.issues.forEach((issue) => {
|
|
110
|
+
console.warn(
|
|
111
|
+
`Warning: Config validation error at '${issue.path.join(".")}': ${issue.message}`
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
console.warn(
|
|
116
|
+
`Warning: Config validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Return defaults on validation failure
|
|
121
|
+
return getDefaultConfig();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Create a default config file at the given path
|
|
127
|
+
* Only creates the file if it doesn't already exist
|
|
128
|
+
* Only triggers for the default path, not custom paths
|
|
129
|
+
*
|
|
130
|
+
* @param configPath - Path where to create the config file
|
|
131
|
+
* @param isDefault - Whether this is the default path (only create if true)
|
|
132
|
+
* @returns true if file was created, false otherwise
|
|
133
|
+
*/
|
|
134
|
+
export async function createDefaultConfig(
|
|
135
|
+
configPath: string,
|
|
136
|
+
isDefault: boolean
|
|
137
|
+
): Promise<boolean> {
|
|
138
|
+
// Only create default config for the default path
|
|
139
|
+
if (!isDefault) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
// Check if file already exists
|
|
145
|
+
await fs.access(configPath);
|
|
146
|
+
// File exists, don't overwrite
|
|
147
|
+
return false;
|
|
148
|
+
} catch {
|
|
149
|
+
// File doesn't exist, create it
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
// Create directory if needed
|
|
154
|
+
const configDir = path.dirname(configPath);
|
|
155
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
156
|
+
|
|
157
|
+
// Get default config and write it
|
|
158
|
+
const defaultConfig = getDefaultConfig();
|
|
159
|
+
const content = JSON.stringify(defaultConfig, null, 2);
|
|
160
|
+
await fs.writeFile(configPath, content, "utf-8");
|
|
161
|
+
|
|
162
|
+
// Log informational message to stderr
|
|
163
|
+
console.error(`Created default config at ${configPath}`);
|
|
164
|
+
|
|
165
|
+
return true;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
// Log warning but don't fail - config will use defaults
|
|
168
|
+
console.warn(
|
|
169
|
+
`Warning: Could not create default config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
170
|
+
);
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zod schema for Phoenix Insight CLI configuration
|
|
5
|
+
*
|
|
6
|
+
* Configuration values can be set via:
|
|
7
|
+
* 1. Config file (~/.phoenix-insight/config.json or custom path)
|
|
8
|
+
* 2. Environment variables (PHOENIX_BASE_URL, PHOENIX_API_KEY, etc.)
|
|
9
|
+
* 3. CLI arguments (--base-url, --api-key, etc.)
|
|
10
|
+
*
|
|
11
|
+
* Priority: config file < env vars < CLI args
|
|
12
|
+
*/
|
|
13
|
+
export const configSchema = z.object({
|
|
14
|
+
/**
|
|
15
|
+
* Phoenix server base URL
|
|
16
|
+
* @default "http://localhost:6006"
|
|
17
|
+
*/
|
|
18
|
+
baseUrl: z.string().default("http://localhost:6006"),
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Phoenix API key for authentication (optional)
|
|
22
|
+
*/
|
|
23
|
+
apiKey: z.string().optional(),
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Maximum number of spans to fetch per project
|
|
27
|
+
* @default 1000
|
|
28
|
+
*/
|
|
29
|
+
limit: z.number().int().positive().default(1000),
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Enable streaming responses from the agent
|
|
33
|
+
* @default true
|
|
34
|
+
*/
|
|
35
|
+
stream: z.boolean().default(true),
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Execution mode: "sandbox" for in-memory filesystem, "local" for real filesystem
|
|
39
|
+
* @default "sandbox"
|
|
40
|
+
*/
|
|
41
|
+
mode: z.enum(["sandbox", "local"]).default("sandbox"),
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Force refresh of snapshot data
|
|
45
|
+
* @default false
|
|
46
|
+
*/
|
|
47
|
+
refresh: z.boolean().default(false),
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Enable tracing of the agent to Phoenix
|
|
51
|
+
* @default false
|
|
52
|
+
*/
|
|
53
|
+
trace: z.boolean().default(false),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Inferred TypeScript type from the config schema
|
|
58
|
+
*/
|
|
59
|
+
export type Config = z.infer<typeof configSchema>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get default configuration values
|
|
63
|
+
*/
|
|
64
|
+
export function getDefaultConfig(): Config {
|
|
65
|
+
return configSchema.parse({});
|
|
66
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./modes/index.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./sandbox.js";
|
|
3
|
+
export * from "./local.js";
|
|
4
|
+
|
|
5
|
+
import { SandboxMode } from "./sandbox.js";
|
|
6
|
+
import { LocalMode } from "./local.js";
|
|
7
|
+
import type { ExecutionMode } from "./types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new sandbox execution mode
|
|
11
|
+
*/
|
|
12
|
+
export function createSandboxMode(): ExecutionMode {
|
|
13
|
+
return new SandboxMode();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a new local execution mode
|
|
18
|
+
*/
|
|
19
|
+
export async function createLocalMode(): Promise<ExecutionMode> {
|
|
20
|
+
return new LocalMode();
|
|
21
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { ExecutionMode } from "./types.js";
|
|
2
|
+
import { exec as execCallback } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import * as fs from "node:fs/promises";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import { tool } from "ai";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
const execAsync = promisify(execCallback);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Local execution mode using real bash and persistent filesystem
|
|
14
|
+
* - Real bash execution via child_process
|
|
15
|
+
* - Persistent storage in ~/.phoenix-insight/
|
|
16
|
+
* - Full system access
|
|
17
|
+
*/
|
|
18
|
+
export class LocalMode implements ExecutionMode {
|
|
19
|
+
private workDir: string;
|
|
20
|
+
private toolCreated = false;
|
|
21
|
+
private bashToolPromise: Promise<any> | null = null;
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
// Create a timestamped directory for this snapshot
|
|
25
|
+
// Add a small random component to ensure uniqueness even if created at the same millisecond
|
|
26
|
+
const timestamp =
|
|
27
|
+
Date.now().toString() + "-" + Math.random().toString(36).substring(7);
|
|
28
|
+
this.workDir = path.join(
|
|
29
|
+
os.homedir(),
|
|
30
|
+
".phoenix-insight",
|
|
31
|
+
"snapshots",
|
|
32
|
+
timestamp,
|
|
33
|
+
"phoenix"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Initialize the working directory
|
|
39
|
+
*/
|
|
40
|
+
private async init() {
|
|
41
|
+
try {
|
|
42
|
+
// Create the directory structure if it doesn't exist
|
|
43
|
+
await fs.mkdir(this.workDir, { recursive: true });
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Failed to initialize local mode directory at ${this.workDir}: ${error instanceof Error ? error.message : String(error)}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async writeFile(filePath: string, content: string): Promise<void> {
|
|
52
|
+
await this.init();
|
|
53
|
+
|
|
54
|
+
// Ensure the path is relative to phoenix root
|
|
55
|
+
const cleanPath = filePath.startsWith("/phoenix")
|
|
56
|
+
? filePath.substring(8) // Remove /phoenix prefix
|
|
57
|
+
: filePath.startsWith("/")
|
|
58
|
+
? filePath.substring(1) // Remove leading slash
|
|
59
|
+
: filePath;
|
|
60
|
+
|
|
61
|
+
const fullPath = path.join(this.workDir, cleanPath);
|
|
62
|
+
|
|
63
|
+
// Create parent directories if they don't exist
|
|
64
|
+
const dirname = path.dirname(fullPath);
|
|
65
|
+
await fs.mkdir(dirname, { recursive: true });
|
|
66
|
+
|
|
67
|
+
// Write the file
|
|
68
|
+
await fs.writeFile(fullPath, content, "utf-8");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async exec(
|
|
72
|
+
command: string
|
|
73
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
74
|
+
await this.init();
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Execute the command in the phoenix directory with a timeout
|
|
78
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
79
|
+
cwd: this.workDir,
|
|
80
|
+
shell: "/bin/bash",
|
|
81
|
+
encoding: "utf-8",
|
|
82
|
+
timeout: 60000, // 60 second timeout for bash commands
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
stdout: stdout || "",
|
|
87
|
+
stderr: stderr || "",
|
|
88
|
+
exitCode: 0,
|
|
89
|
+
};
|
|
90
|
+
} catch (error: any) {
|
|
91
|
+
// Handle command execution errors
|
|
92
|
+
if (error.code !== undefined) {
|
|
93
|
+
return {
|
|
94
|
+
stdout: error.stdout || "",
|
|
95
|
+
stderr: error.stderr || error.message || "Command failed",
|
|
96
|
+
exitCode: error.code || 1,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Handle other errors
|
|
101
|
+
return {
|
|
102
|
+
stdout: "",
|
|
103
|
+
stderr: error.message || "Unknown error",
|
|
104
|
+
exitCode: 1,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async getBashTool(): Promise<any> {
|
|
110
|
+
// Only create the tool once and cache it
|
|
111
|
+
if (!this.bashToolPromise) {
|
|
112
|
+
this.bashToolPromise = this.createBashTool();
|
|
113
|
+
}
|
|
114
|
+
return this.bashToolPromise;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private async createBashTool(): Promise<any> {
|
|
118
|
+
// We can't use bash-tool directly for local mode since it's designed for just-bash
|
|
119
|
+
// Instead, we'll create a tool using the AI SDK's tool function
|
|
120
|
+
// This ensures compatibility with the AI SDK
|
|
121
|
+
|
|
122
|
+
// Return a bash tool that executes real bash commands
|
|
123
|
+
return tool({
|
|
124
|
+
description: "Execute bash commands in the local filesystem",
|
|
125
|
+
inputSchema: z.object({
|
|
126
|
+
command: z.string().describe("The bash command to execute"),
|
|
127
|
+
}),
|
|
128
|
+
execute: async ({ command }) => {
|
|
129
|
+
const result = await this.exec(command);
|
|
130
|
+
|
|
131
|
+
// Return result in a format similar to bash-tool
|
|
132
|
+
if (result.exitCode !== 0) {
|
|
133
|
+
// Include error details in the response
|
|
134
|
+
return {
|
|
135
|
+
success: false,
|
|
136
|
+
stdout: result.stdout,
|
|
137
|
+
stderr: result.stderr,
|
|
138
|
+
exitCode: result.exitCode,
|
|
139
|
+
error: `Command failed with exit code ${result.exitCode}`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
success: true,
|
|
145
|
+
stdout: result.stdout,
|
|
146
|
+
stderr: result.stderr,
|
|
147
|
+
exitCode: result.exitCode,
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async cleanup(): Promise<void> {
|
|
154
|
+
// Optional: Clean up old snapshots
|
|
155
|
+
// For now, we'll keep all snapshots for user reference
|
|
156
|
+
// Users can manually clean ~/.phoenix-insight/ if needed
|
|
157
|
+
// We could implement logic to:
|
|
158
|
+
// 1. Keep only the last N snapshots
|
|
159
|
+
// 2. Delete snapshots older than X days
|
|
160
|
+
// 3. Provide a separate cleanup command
|
|
161
|
+
// For this implementation, we do nothing
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { ExecutionMode } from "./types.js";
|
|
2
|
+
import { tool } from "ai";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sandbox execution mode using just-bash for isolated execution
|
|
7
|
+
* - In-memory filesystem
|
|
8
|
+
* - Simulated bash commands
|
|
9
|
+
* - No disk or network access
|
|
10
|
+
*/
|
|
11
|
+
export class SandboxMode implements ExecutionMode {
|
|
12
|
+
private bash: any; // Will be typed as Bash from just-bash
|
|
13
|
+
private initialized = false;
|
|
14
|
+
private bashToolPromise: Promise<any> | null = null;
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
// We'll initialize in the init method since we need async imports
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private async init() {
|
|
21
|
+
if (this.initialized) return;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// Dynamic imports for ESM modules
|
|
25
|
+
const { Bash } = await import("just-bash");
|
|
26
|
+
|
|
27
|
+
// Initialize just-bash with /phoenix as the working directory
|
|
28
|
+
this.bash = new Bash({ cwd: "/phoenix" });
|
|
29
|
+
|
|
30
|
+
this.initialized = true;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Failed to initialize sandbox mode: ${error instanceof Error ? error.message : String(error)}`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async writeFile(path: string, content: string): Promise<void> {
|
|
39
|
+
await this.init();
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Ensure the path starts with /phoenix
|
|
43
|
+
const fullPath = path.startsWith("/phoenix")
|
|
44
|
+
? path
|
|
45
|
+
: `/phoenix${path.startsWith("/") ? "" : "/"}${path}`;
|
|
46
|
+
|
|
47
|
+
// Create parent directories if they don't exist
|
|
48
|
+
const dirname = fullPath.substring(0, fullPath.lastIndexOf("/"));
|
|
49
|
+
if (dirname) {
|
|
50
|
+
await this.bash.exec(`mkdir -p ${dirname}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Write the file using just-bash's filesystem
|
|
54
|
+
// We'll use the InMemoryFs directly for better performance
|
|
55
|
+
this.bash.fs.writeFileSync(fullPath, content);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Failed to write file ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async exec(
|
|
64
|
+
command: string
|
|
65
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
66
|
+
await this.init();
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const result = await this.bash.exec(command);
|
|
70
|
+
|
|
71
|
+
// just-bash returns a different structure, so we need to normalize it
|
|
72
|
+
return {
|
|
73
|
+
stdout: result.stdout || "",
|
|
74
|
+
stderr: result.stderr || "",
|
|
75
|
+
exitCode: result.exitCode || 0,
|
|
76
|
+
};
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// If the command fails, just-bash throws an error
|
|
79
|
+
// Extract what we can from the error
|
|
80
|
+
if (error && typeof error === "object" && "exitCode" in error) {
|
|
81
|
+
return {
|
|
82
|
+
stdout: (error as any).stdout || "",
|
|
83
|
+
stderr: (error as any).stderr || error.toString(),
|
|
84
|
+
exitCode: (error as any).exitCode || 1,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Fallback for unexpected errors
|
|
89
|
+
return {
|
|
90
|
+
stdout: "",
|
|
91
|
+
stderr: error?.toString() || "Unknown error",
|
|
92
|
+
exitCode: 1,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async getBashTool(): Promise<any> {
|
|
98
|
+
// Only create the tool once and cache it
|
|
99
|
+
if (!this.bashToolPromise) {
|
|
100
|
+
this.bashToolPromise = this.createBashTool();
|
|
101
|
+
}
|
|
102
|
+
return this.bashToolPromise;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private async createBashTool(): Promise<any> {
|
|
106
|
+
await this.init();
|
|
107
|
+
|
|
108
|
+
// Create a bash tool compatible with the AI SDK
|
|
109
|
+
// Similar to local mode, we'll create it directly using the tool function
|
|
110
|
+
return tool({
|
|
111
|
+
description: "Execute bash commands in the sandbox filesystem",
|
|
112
|
+
inputSchema: z.object({
|
|
113
|
+
command: z.string().describe("The bash command to execute"),
|
|
114
|
+
}),
|
|
115
|
+
execute: async ({ command }) => {
|
|
116
|
+
const result = await this.exec(command);
|
|
117
|
+
|
|
118
|
+
// Return result in a format similar to bash-tool
|
|
119
|
+
if (result.exitCode !== 0) {
|
|
120
|
+
// Include error details in the response
|
|
121
|
+
return {
|
|
122
|
+
success: false,
|
|
123
|
+
stdout: result.stdout,
|
|
124
|
+
stderr: result.stderr,
|
|
125
|
+
exitCode: result.exitCode,
|
|
126
|
+
error: `Command failed with exit code ${result.exitCode}`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
success: true,
|
|
132
|
+
stdout: result.stdout,
|
|
133
|
+
stderr: result.stderr,
|
|
134
|
+
exitCode: result.exitCode,
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async cleanup(): Promise<void> {
|
|
141
|
+
// No-op for in-memory mode - garbage collection will handle cleanup
|
|
142
|
+
// We could optionally clear the filesystem here if needed
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common interface for execution modes (sandbox vs local)
|
|
3
|
+
*/
|
|
4
|
+
export interface ExecutionMode {
|
|
5
|
+
/**
|
|
6
|
+
* Write Phoenix data to the filesystem
|
|
7
|
+
* @param path - The file path relative to the Phoenix root
|
|
8
|
+
* @param content - The content to write
|
|
9
|
+
*/
|
|
10
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Execute a bash command and return output
|
|
14
|
+
* @param command - The bash command to execute
|
|
15
|
+
* @returns The command output with stdout, stderr, and exit code
|
|
16
|
+
*/
|
|
17
|
+
exec(
|
|
18
|
+
command: string
|
|
19
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the bash tool for the AI SDK agent
|
|
23
|
+
* @returns A tool that can be used by the AI SDK
|
|
24
|
+
*/
|
|
25
|
+
getBashTool(): Promise<any>; // Tool type from AI SDK
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Clean up resources
|
|
29
|
+
*/
|
|
30
|
+
cleanup(): Promise<void>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phoenix Insight observability configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { register, type NodeTracerProvider } from "@arizeai/phoenix-otel";
|
|
6
|
+
import { DiagLogLevel } from "@opentelemetry/api";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Options for configuring observability
|
|
10
|
+
*/
|
|
11
|
+
export interface ObservabilityOptions {
|
|
12
|
+
/** Whether to enable tracing */
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
/** Phoenix base URL for sending traces */
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
/** Phoenix API key for authentication */
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
/** Phoenix project name for organizing traces */
|
|
19
|
+
projectName?: string;
|
|
20
|
+
/** Whether to enable debug logging */
|
|
21
|
+
debug?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Global tracer provider instance
|
|
26
|
+
*/
|
|
27
|
+
let tracerProvider: NodeTracerProvider | null = null;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if observability is enabled
|
|
31
|
+
*/
|
|
32
|
+
export function isObservabilityEnabled(): boolean {
|
|
33
|
+
return tracerProvider !== null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Initialize observability for the Phoenix Insight agent
|
|
38
|
+
*/
|
|
39
|
+
export function initializeObservability(options: ObservabilityOptions): void {
|
|
40
|
+
if (!options.enabled) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// If already initialized, skip
|
|
45
|
+
if (tracerProvider) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Configure the tracer provider
|
|
51
|
+
tracerProvider = register({
|
|
52
|
+
projectName: options.projectName || "phoenix-insight",
|
|
53
|
+
url: options.baseUrl,
|
|
54
|
+
apiKey: options.apiKey,
|
|
55
|
+
batch: true,
|
|
56
|
+
global: true,
|
|
57
|
+
diagLogLevel: options.debug ? DiagLogLevel.DEBUG : undefined,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (options.debug) {
|
|
61
|
+
console.error(
|
|
62
|
+
"🔭 Observability enabled - traces will be sent to Phoenix"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error("⚠️ Failed to initialize observability:", error);
|
|
67
|
+
// Don't throw - observability should not break the main functionality
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Shutdown observability and cleanup resources
|
|
73
|
+
*/
|
|
74
|
+
export async function shutdownObservability(): Promise<void> {
|
|
75
|
+
if (tracerProvider) {
|
|
76
|
+
try {
|
|
77
|
+
await tracerProvider.shutdown();
|
|
78
|
+
tracerProvider = null;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error("⚠️ Error shutting down observability:", error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get the current tracer provider
|
|
87
|
+
*/
|
|
88
|
+
export function getTracerProvider(): NodeTracerProvider | null {
|
|
89
|
+
return tracerProvider;
|
|
90
|
+
}
|